Home Статьи От механика до программиста: как следование своей мечте изменило мою жизнь

От механика до программиста: как следование своей мечте изменило мою жизнь

by admin

Развитие управляемых облачных сервисов, облачных и бессерверных приложений приносит как новые возможности, так и проблемы. Все больше и больше методов из процессов разработки программного обеспечения, таких как контроль версий, проверка кода, непрерывная интеграция и автоматическое тестирование, применяются к автоматизации облачной инфраструктуры.

Большинство существующих инструментов предлагают определять инфраструктуру в текстовых форматах разметки, наиболее предпочтительным из которых является YAML. В этой статье я привожу аргументы в пользу использования настоящих языков программирования, таких как TypeScript. Такое изменение делает еще больше методов разработки программного обеспечения применимыми к сфере инфраструктуры.

Образец заявки

Легче обосновать на конкретном примере. В этой статье мы создадим приложение для сокращения URL-адресов, базовый клон tinyurl.com или bit.ly. Существует административная страница, где мы можем определить короткие псевдонимы для длинных URL-адресов:

0*вУДдеСИДФоГп2УДЕ
Пример приложения для сокращения URL-адресов

Теперь всякий раз, когда посетитель переходит по базовому URL-адресу приложения + существующему псевдониму, он перенаправляется на полный URL-адрес.

Это приложение просто описать, но оно включает в себя достаточно движущихся частей, чтобы отражать некоторые проблемы реального мира. В качестве бонуса в Интернете есть множество реализаций, с которыми можно сравнить.

Бессерверный сокращатель URL-адресов

Я большой сторонник бессерверной архитектуры: стиль облачных приложений представляет собой комбинацию бессерверных функций и управляемых облачных сервисов. Они быстро разрабатываются, легко запускаются и стоят копейки, если приложение не получает много пользователей. Однако даже бессерверные приложения должны иметь дело с инфраструктурой, такой как базы данных, очереди и другие источники событий и места назначения данных.

В моих примерах будет использоваться Amazon AWS, но это также может быть Microsoft Azure или Google Cloud Platform.

Итак, суть заключается в том, чтобы хранить URL-адреса с короткими именами в виде пар ключ-значение в Amazon DynamoDB и использовать AWS Lambdas для запуска кода приложения. Вот первоначальный набросок:

0*IHX7dL1m1H8uvcF4
Сокращение URL-адресов с помощью AWS Lambda и DynamoDB

Лямбда вверху получает событие, когда кто-то решает добавить новый URL. Он извлекает имя и URL-адрес из запроса и сохраняет их как элемент в таблице DynamoDB.

Lambda внизу вызывается всякий раз, когда пользователь переходит по короткому URL-адресу. Код считывает полный URL-адрес на основе запрошенного пути и возвращает ответ 301 с соответствующим местоположением.

Вот реализация Open URL Лямбда в JavaScript:

const aws = require('aws-sdk');
const table = new aws.DynamoDB.DocumentClient();
exports.handler = async (event) => {
  const name = event.path.substring(1);
  const params = { TableName: "urls", Key: { "name": name } };
  const value = await table.get(params).promise();
  const url = value && value.Item && value.Item.url;
  return url
    ? { statusCode: 301, body: "", headers: { "Location": url } }
    : { statusCode: 404, body: name + " not found" };
};

Это 11 строк кода. я пропущу реализацию Add URL функция, потому что она очень похожа. Учитывая третью функцию для вывода списка существующих URL-адресов для пользовательского интерфейса, у нас может получиться всего 30-40 строк JavaScript.

Итак, как нам развернуть приложение?

Что ж, прежде чем мы это сделаем, мы должны понять, что приведенная выше картинка была чрезмерным упрощением:

  • AWS Lambda не может напрямую обрабатывать HTTP-запросы, поэтому перед ним необходимо добавить AWS API Gateway.
  • Нам также нужно предоставить некоторые статические файлы для пользовательского интерфейса, которые мы поместим в AWS S3 и проксируем их с помощью того же шлюза API.

Вот обновленная схема:

0*2ezVdcpK_Wt9HdhM
Шлюз API, Lambda, DynamoDB и S3

Это жизнеспособная конструкция, но детали еще сложнее:

  • Шлюз API — это сложный зверь, который требует правильной настройки этапов, развертываний и конечных точек REST.
  • Разрешения и политики должны быть определены, чтобы шлюз API мог вызывать Lambda, а Lambda могла получить доступ к DynamoDB.
  • Статические файлы должны находиться в объектах корзины S3.

Итак, фактическая настройка включает в себя пару десятков объектов, которые нужно настроить в AWS:

0*8vOMSf1GN9eHgrKw
Все облачные ресурсы должны быть предоставлены

Как мы подходим к этой задаче?

Варианты предоставления инфраструктуры

Существует множество вариантов подготовки облачного приложения, и каждый из них имеет свои недостатки. Давайте быстро пройдемся по списку возможностей для понимания ландшафта.

Веб-консоль AWS

AWS, как и любое другое облако, имеет web user interface настроить его ресурсы:

0*RzAUosMerv055fWr
Веб-консоль AWS

Это хорошее место для начала — хорошо для экспериментов, выяснения доступных опций, следования инструкциям, т. е. для исследования.

Однако он не особенно подходит для долгоживущих постоянно меняющихся приложений, разрабатываемых в группах. Развертывание вручную довольно сложно воспроизвести в точности, что довольно быстро становится проблемой сопровождения.

Интерфейс командной строки AWS

AWS Command Line Interface (CLI) — это унифицированный инструмент для управления всеми сервисами AWS из командной строки. Вы пишете вызовы так:

aws apigateway create-rest-api --name 'My First API' --description 'This is my first API'
aws apigateway create-stage --rest-api-id 1234123412 --stage-name 'dev' --description 'Development stage' --deployment-id a1b2c3

Первый опыт может быть не таким гладким, как нажатие кнопок в браузере, но огромное преимущество заключается в том, что вы можете снова использовать команды, которые вы когда-то написали. Вы можете создавать сценарии, объединяя множество команд в связные сценарии. Таким образом, ваш коллега может воспользоваться тем же сценарием, который создали вы. Вы можете подготовить несколько сред, настроив сценарии.

Честно говоря, я никогда этого не делал по нескольким причинам:

  • CLI-скрипты кажутся мне слишком важными. Я должен описывать «как» что-то делать, а не «что» я хочу получить в итоге.
  • Кажется, нет хорошей истории для обновления существующих ресурсов. Пишу ли я небольшие дельта-скрипты для каждого изменения? Должен ли я хранить их навсегда и запускать полный набор каждый раз, когда мне нужна новая среда?
  • Если в процессе выполнения сценария произойдет сбой, мне нужно будет вручную восстановить все до согласованного состояния. Это очень быстро становится грязным, и у меня нет желания использовать этот процесс, особенно в продакшене.

Чтобы преодолеть такие ограничения, понятие Desired State Configuration (ДСК) был изобретен. В соответствии с этой парадигмой мы описываем желаемую схему инфраструктуры, а затем инструментарий заботится либо о ее подготовке с нуля, либо о внесении необходимых изменений в существующую среду.

Какой инструмент предоставляет модель DSC для AWS? Есть легионы.

AWS CloudFormation

AWS CloudFormation — это собственный инструмент для управления Desired State Configuration от Amazon. Шаблоны CloudFormation используют YAML для описания всех ресурсов инфраструктуры AWS.

Вот отрывок из a private URL shortener example любезно предоставлено в блоге AWS:

Resources:
  S3BucketForURLs:
  Type: "AWS::S3::Bucket"
  DeletionPolicy: Delete
  Properties:
    BucketName: !If ["CreateNewBucket", !Ref "AWS::NoValue", !Ref S3BucketName ]
    WebsiteConfiguration:
      IndexDocument: "index.html"
    LifecycleConfiguration:
      Rules:
        -
          Id: DisposeShortUrls
          ExpirationInDays: !Ref URLExpiration
          Prefix: "u"
         Status: Enabled

Это всего лишь очень короткий фрагмент: полный пример состоит из 317 строк YAML. Это на порядок больше, чем реальный код JavaScript, который у нас есть в приложении!

CloudFormation — мощный инструмент, но для его освоения требуется некоторое обучение. Более того, это специфично для AWS: вы не сможете передать навык другим облачным провайдерам.

Было бы здорово, если бы существовал универсальный формат DSC? Встречайте Терраформ.

Terraform

HashiCorp Terraform — это инструмент с открытым исходным кодом для определения инфраструктуры в декларативных файлах конфигурации. Он имеет подключаемую архитектуру, поэтому инструмент поддерживает все основные облака и даже гибридные сценарии.

Пользовательский текстовый Terraform .tf формат используется для определения конфигураций. Язык шаблонов довольно мощный, и как только вы его изучите, вы сможете использовать его для разных облачных провайдеров.

Вот отрывок из AWS Lambda Short URL Generator пример:

resource "aws_api_gateway_rest_api" "short_urls_api_gateway" {
  name        = "Short URLs API"
  description = "API for managing short URLs."
}
resource "aws_api_gateway_usage_plan" "short_urls_api_usage_plan" {
  name         = "Short URLs admin API key usage plan"
  description  = "Usage plan for the admin API key for Short URLS."
  api_stages {
    api_id = "${aws_api_rest_api.short_urls_gateway.id}"
    stage  = "${aws_api_deployment.short_url_deployment.stage_name}"
  }
}

На этот раз полный пример составляет около 450 строк текстовых шаблонов. Существуют ли способы уменьшить размер определения инфраструктуры?

Да, повышая уровень абстракции. Это возможно с модулями Terraform или с помощью других, более специализированных инструментов.

Бессерверная среда и SAM

The Serverless Framework — это инструмент управления инфраструктурой, ориентированный на бессерверные приложения. Он работает со всеми облачными провайдерами (хотя поддержка AWS является самой сильной) и предоставляет только функции, связанные с созданием приложений с облачными функциями.

Преимущество в том, что это намного более лаконично. Еще раз, инструмент использует YAML для определения шаблонов, вот фрагмент из Serverless URL Shortener пример:

functions:
  store:
    handler: api/store.handle
    events:
      - http:
          path: /
          method: post
          cors: true

Язык предметной области дает более короткое определение: в этом примере 45 строк YAML + 123 строки функций JavaScript.

Однако у краткости есть обратная сторона: как только вы отклоняетесь от довольно «тонкого» золотого пути — облачных функций и неполного списка источников событий — вам приходится прибегать к более универсальным инструментам, таким как CloudFormation. Как только ваш ландшафт включает работу с инфраструктурой более низкого уровня или некоторые компоненты на основе контейнеров, вы снова застреваете в использовании нескольких языков конфигурации и инструментов.

Амазонки AWS Serverless Application Model (SAM) очень похож на Serverless Framework, но адаптирован для работы с AWS.

Это конец игры? Я так не думаю.

Желаемые свойства инструмента определения инфраструктуры

Итак, что мы узнали, пройдя через существующий ландшафт? Идеальные инструменты инфраструктуры должны:

  • Обеспечивать reproducible результаты развертывания
  • Be scriptable, т. е. не требуют вмешательства человека после завершения определения
  • Определите desired state а не конкретные шаги для его достижения
  • Поддержка multiple cloud providers и гибридные сценарии
  • Be universal в смысле использования одного и того же инструмента для определения любого типа ресурса
  • Be succinct и concise оставаться читабельным и управляемым
  • ̶U̶̶̶s̶̶̶e̶̶̶ ̶̶̶y̶̶̶a̶̶̶m̶̶̶l̶̶̶-̶̶̶b̶̶̶a̶̶̶s̶̶̶e̶̶̶d̶̶̶ ̶̶̶f̶̶̶o̶̶̶r̶̶̶m̶̶̶a̶̶̶t̶̶̶

Нет, я вычеркнул последний пункт. YAML кажется самым популярным языком среди инструментов этого класса (и я еще даже не прикасался к Kubernetes!), но я не уверен, что он хорошо работает для меня. YAML has many flaws, and I just don’t want to use it.

Вы заметили, что я не упомянул Infrastructure as code еще один раз? Ну вот (от Wikipedia):

Инфраструктура как код (IaC) — это процесс управления компьютерными центрами обработки данных и их предоставления с помощью машиночитаемых файлов определений, а не физической конфигурации оборудования или интерактивных инструментов настройки.

Разве он не должен называться «Инфраструктура как файлы определений» или «Инфраструктура как YAML»?

Как разработчик программного обеспечения, я действительно хочу, чтобы «инфраструктура как реальный код, вы знаете, программная вещь». я хочу использовать the same language что я уже знаю. Я хочу остаться в том же редакторе. Я хочу получить IntelliSense auto-completion когда я печатаю. я хочу увидеть compilation errors когда то, что я набрал, синтаксически неверно. Я хочу повторно использовать developer skills что у меня уже есть. я хочу придумать abstractions обобщить мой код и создать reusable components. Я хочу leverage the open-source community который создал бы намного лучшие компоненты, чем я когда-либо мог. я бы хотел combine the code and infrastructure в одном кодовом проекте.

Если вы со мной в этом, продолжайте читать. Вы получаете все это с Пулуми.

Пулуми

Pulumi — это инструмент для создания облачного программного обеспечения с использованием реальных языков программирования. Они поддерживают всех основных облачных провайдеров, а также Kubernetes.

Модель программирования Pulumi также поддерживает Go и Python, но в оставшейся части статьи я буду использовать TypeScript.

Создавая прототип сокращателя URL-адресов, я объясняю основной способ работы и иллюстрирую преимущества и некоторые компромиссы. Если вы хотите следовать вместе, install Pulumi.

Как работает пулуми

Давайте начнем определять наше приложение для сокращения URL-адресов в TypeScript. я установил @pulumi/pulumi и @pulumi/aws Модули NPM, чтобы я мог запустить программу. Первым ресурсом, который нужно создать, является таблица DynamoDB:


import * as aws from "@pulumi/aws";
// A DynamoDB table with a single primary key
let counterTable = new aws.dynamodb.Table("urls", {
    name: "urls",
    attributes: [
        { name: "name", type: "S" },
    ],
    hashKey: "name",
    readCapacity: 1,
    writeCapacity: 1
});

я использую pulumi CLI для запуска этой программы для предоставления фактического ресурса в AWS:

> pulumi up
Previewing update (urlshortener):
   Type                  Name           Plan
+  pulumi:pulumi:Stack   urlshortener   create
+    aws:dynamodb:Table  urls           create
Resources:
    + 2 to create
Do you want to perform this update? yes
Updating (urlshortener):
   Type                  Name           Status
+  pulumi:pulumi:Stack   urlshortener   created
+    aws:dynamodb:Table  urls           created
Resources:
    + 2 created

CLI сначала показывает предварительный просмотр изменений, которые необходимо внести, а когда я подтверждаю, он создает ресурс. Он также создает stack — контейнер для всех ресурсов приложения.

Этот код может выглядеть как императивная команда для создания таблицы DynamoDB, но на самом деле это не так. Если я пойду дальше и изменюсь readCapacity в 2 а затем повторно запустить pulumi up, это дает другой результат:

> pulumi up
Previewing update (urlshortener):
   Type                  Name           Plan
   pulumi:pulumi:Stack   urlshortener
~    aws:dynamodb:Table  urls           update [diff: ~readCapacity]
Resources:
    ~ 1 to update 1 unchanged

Он обнаруживает точное изменение, которое я сделал, и предлагает обновление. На следующем рисунке показано, как работает Pulumi:

0*viskl_7ZYjUekaYZ
Как работает пулуми

index.ts в красном квадрате моя программа. Языковой хост Pulumi понимает TypeScript и переводит код в команды для внутреннего движка. В результате движок строит дерево ресурсов, подлежащих предоставлению, желаемое состояние инфраструктуры.

Конечное состояние последнего развертывания сохраняется в хранилище (может быть в бэкэнде pulumi.com или в файле на диске). Затем движок сравнивает текущее состояние системы с желаемым состоянием программы и вычисляет дельту с точки зрения команд создания-обновления-удаления для облачного провайдера.

Справка по типам

Теперь я могу перейти к коду, определяющему лямбда-функцию:

// Create a Role giving our Lambda access.
let policy: aws.iam.PolicyDocument = { /* Redacted for brevity */ };
let role = new aws.iam.Role("lambda-role", {
    assumeRolePolicy: JSON.stringify(policy),
});
let fullAccess = new aws.iam.RolePolicyAttachment("lambda-access", {
    role: role,
    policyArn: aws.iam.AWSLambdaFullAccess,
});
// Create a Lambda function, using code from the `./app` folder.
let lambda = new aws.lambda.Function("lambda-get", {
    runtime: aws.lambda.NodeJS8d10Runtime,
    code: new pulumi.asset.AssetArchive({
        ".": new pulumi.asset.FileArchive("./app"),
    }),
    timeout: 300,
    handler: "read.handler",
    role: role.arn,
    environment: {
        variables: {
            "COUNTER_TABLE": counterTable.name
        }
    },
}, { dependsOn: [fullAccess] });

Вы можете видеть, что сложность возрастает, а размер кода растет. Однако теперь я начинаю получать реальные преимущества от использования типизированного языка программирования:

  • Я использую объекты в определениях других параметров объекта. Если я напишу их имя с ошибкой, я не получу сбой во время выполнения, а немедленное сообщение об ошибке от редактора.
  • Если я не знаю, какие параметры мне нужно предоставить, я могу перейти к определению типа и посмотреть его (или использовать IntelliSense).
  • Если я забуду указать обязательный параметр, я получу явную ошибку.
  • Если тип входного параметра не соответствует типу передаваемого объекта, я снова получаю ошибку.
  • Я могу использовать языковые функции, такие как JSON.stringify прямо внутри моей программы. На самом деле, я могу ссылаться и использовать любой модуль NPM.

Вы можете увидеть код для шлюза API here. Это выглядит слишком многословно, не так ли? Более того, я только на полпути с определением только одной лямбда-функции.

Многоразовые компоненты

Мы можем сделать лучше, чем это. Вот улучшенное определение той же лямбда-функции:

import { Lambda } from "./lambda";
const func = new Lambda("lambda-get", {
    path: "./app",
    file: "read",
    environment: {
       "COUNTER_TABLE": counterTable.name
    },
});

Разве это не прекрасно? Остались только основные опции, а вся техника исчезла. Ну, он не исчез полностью, он был спрятан за абстракция.

Я определил custom component под названием Lambda:

export interface LambdaOptions {
    readonly path: string;
    readonly file: string;
    readonly environment?:  pulumi.Input<{
        [key: string]: pulumi.Input<string>;
    }>;
}
export class Lambda extends pulumi.ComponentResource {
    public readonly lambda: aws.lambda.Function;
    constructor(name: string,
        options: LambdaOptions,
        opts?: pulumi.ResourceOptions) {
        super("my:Lambda", name, opts);
        const role = //... Role as defined in the last snippet
        const fullAccess = //... RolePolicyAttachment as defined in the last snippet
        this.lambda = new aws.lambda.Function(`${name}-func`, {
            runtime: aws.lambda.NodeJS8d10Runtime,
            code: new pulumi.asset.AssetArchive({
                ".": new pulumi.asset.FileArchive(options.path),
            }),
            timeout: 300,
            handler: `${options.file}.handler`,
            role: role.arn,
            environment: {
                variables: options.environment
            }
        }, { dependsOn: [fullAccess], parent: this });
    }
}

Интерфейс LambdaOptions определяет параметры, которые важны для моей абстракции. Класс Lambda происходит от pulumi.ComponentResource и создает все дочерние ресурсы в своем конструкторе.

Приятным эффектом является то, что можно увидеть структуру в pulumi предварительный просмотр:

> pulumi up
Previewing update (urlshortener):
   Type                              Name               Plan
+  pulumi:pulumi:Stack               urlshortener       create
+    my:Lambda                       lambda-get         create
+      aws:iam:Role                  lambda-get-role    create
+      aws:iam:RolePolicyAttachment  lambda-get-access  create
+      aws:lambda:Function           lambda-get-func    create
+    aws:dynamodb:Table              urls               create

Endpoint компонент упрощает определение шлюза API (см. the source):

const api = new Endpoint("urlapi", {
    path: "/{proxy+}",
    lambda: func.lambda
});

Компонент скрывает сложность от клиентов — если абстракция была выбрана правильно, т.е. Класс компонента можно повторно использовать в нескольких местах, в нескольких проектах, в разных командах и т. д.

Стандартная библиотека компонентов

На самом деле команда Pulumi придумала множество высокоуровневых компонентов, которые строят абстракции поверх необработанных ресурсов. Компоненты из @pulumi/cloud-aws package особенно полезны для бессерверных приложений.

Вот полное приложение для сокращения URL-адресов с таблицей DynamoDB, Lambdas, шлюзом API и статическими файлами на основе S3:

import * as aws from "@pulumi/cloud-aws";
// Create a table `urls`, with `name` as primary key.
let urlTable = new aws.Table("urls", "name");
// Create a web server.
let endpoint = new aws.API("urlshortener");
// Serve all files in the www directory to the root.
endpoint.static("/", "www");
// GET /url/{name} redirects to the target URL based on a short-name.
endpoint.get("/url/{name}", async (req, res) => {
    let name = req.params["name"];
    let value = await urlTable.get({name});
    let url = value && value.url;
    // If we found an entry, 301 redirect to it; else, 404.
    if (url) {
        res.setHeader("Location", url);
        res.status(301);
        res.end("");
    }
    else {
        res.status(404);
        res.end("");
    }
});
// POST /url registers a new URL with a given short-name.
endpoint.post("/url", async (req, res) => {
    let url = req.query["url"];
    let name = req.query["name"];
    await urlTable.insert({ name, url });
    res.json({ shortenedURLName: name });
});
export let endpointUrl = endpoint.publish().url;

Самое крутое здесь то, что на самом деле код реализации AWS Lambdas — это intertwined с определение ресурсов. Код очень похож на экспресс-приложение. Лямбда-выражения AWS определяются как лямбда-выражения TypeScript. Все строго типизировано и проверено во время компиляции.

Стоит отметить, что на данный момент такие высокоуровневые компоненты существуют только в TypeScript. Можно создавать собственные компоненты на Python или Go, но стандартной библиотеки нет. Пулуми, ребята are actively trying to figure out a way to bridge this gap.

Как избежать привязки к поставщику?

Если вы внимательно посмотрите на предыдущий блок кода, то заметите, что только одна строка специфична для AWS: import утверждение. Остальное только именование.

Мы можем избавиться и от этого: просто измените импорт на import * as cloud from "@pulumi/cloud"; и заменить aws. с cloud. где угодно. Теперь нам нужно перейти в файл конфигурации стека и указать там облачного провайдера:

config:
  cloud:provider: aws

Этого достаточно, чтобы приложение снова заработало!

Привязка к поставщику, по-видимому, вызывает у многих большую озабоченность, когда речь идет об облачных архитектурах, в значительной степени зависящих от управляемых облачных сервисов, включая бессерверные приложения. Хотя я не обязательно разделяю эти опасения и не уверен, что общие абстракции — это правильный путь, библиотека Pulumi Cloud может быть одним из направлений для исследования.

Следующая картинка иллюстрирует выбор уровня абстракции, который обеспечивает Pulumi:

1*tyZNF_k6q7gNWHwsjLFb5w
Слои абстракции пулуми

Работая поверх API поставщика облачных услуг и внутреннего поставщика ресурсов, вы можете работать с необработанными компонентами с максимальной гибкостью или согласиться на абстракции более высокого уровня. Также возможно смешивание в одной программе.

Инфраструктура как настоящий код

Разработка приложений для современного облака означает использование нескольких облачных сервисов, которые необходимо настроить для совместной работы. Подход «Инфраструктура как код» является почти обязательным требованием для обеспечения надежного управления такими приложениями в условиях группы и в течение длительного периода времени.

Код приложения и поддерживающая инфраструктура становятся все более и более комбинированными, поэтому естественно, что разработчики программного обеспечения берут на себя ответственность за определение того и другого. Следующим логическим шагом является использование одного и того же набора языков, инструментов и методов как для программного обеспечения, так и для инфраструктуры.

Pulumi предоставляет облачные ресурсы в виде API на нескольких популярных языках программирования общего назначения. Разработчики могут напрямую передавать свои навыки и опыт для определения, создания, компоновки и развертывания современных облачных и бессерверных приложений более эффективно, чем когда-либо.

Первоначально опубликовано в mikhail.io.

ВАМ ТАКЖЕ МОЖЕТ ПОНРАВИТЬСЯ

ОСТАВИТЬ КОММЕНТАРИЙ