Русский
Русский
English
Статистика
Реклама

Lambda

Kotlin. Лямбда vs Ссылка на функцию

10.03.2021 12:22:46 | Автор: admin

Kotlin уже давно стал основным языком программирования на Android. Одна из причин, почему мне нравится этот язык, это то, что функции в нем являются объектами первого класса. То есть функцию можно передать как параметр, использовать как возвращаемое значение и присвоить переменной. Также вместо функции можно передать так называемую лямбду. И недавно у меня возникла интересная проблема, связанная с заменой лямбды ссылкой на функцию.

Представим, что у нас есть класс Button, который в конструкторе получает как параметр функцию onClick

class Button(    private val onClick: () -> Unit) {    fun performClick() = onClick()}

И есть класс ButtonClickListener, который реализует логику нажатий на кнопку

class ButtonClickListener {    fun onClick() {        print("Кнопка нажата")    }}

В классе ScreenView у нас хранится переменная lateinit var listener: ButtonClickListener и создается кнопка, которой передается лямбда, внутри которой вызывается метод ButtonClickListener.onClick

class ScreenView {    lateinit var listener: ButtonClickListener    val button = Button { listener.onClick() }}

В методе main создаем объект ScreenView, инициализируем переменную listener и имитируем нажатие по кнопке

fun main() {    val screenView = ScreenView()    screenView.listener = ButtonClickListener()    screenView.button.performClick()}

После запуска приложения, все нормально отрабатывает и выводится строка "Кнопка нажата".

А теперь давайте вернемся в класс ScreenView и посмотрим на строку, где создается кнопка - val button = Button { listener.onClick() }. Вы могли заметить, что метод ButtonClickListener.onClick по сигнатуре схож с функцией onClick: () -> Unit, которую принимает конструктор нашей кнопки, а это значит, что мы можем заменить лямбда выражение ссылкой на функцию. В итоге получим

class ScreenView {    lateinit var listener: ButtonClickListener    val button = Button(listener::onClick)}

Но при запуске программа вылетает со следующей ошибкой - поле listener не инициализированно

Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property listener has not been initializedat lambdas.ScreenView.<init>(ScreenView.kt:6)at lambdas.ScreenViewKt.main(ScreenView.kt:10)at lambdas.ScreenViewKt.main(ScreenView.kt)

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

При использовании лямбды создается анонимный класс Function0 и в методе invoke вызывается код, который мы передали в нашу лямбду. В нашем случае - listener.onClick()

private final Button button = new Button((Function0)(new Function0() {    public final void invoke() {       ScreenView.this.getListener().onClick();    }}));

То есть если мы передаем лямбду, наша переменная listener будет использована после имитации нажатия и она уже будет инициализирована.

А вот что происходит при использовании ссылки на функцию. Тут также создается анонимный класс Function0, но если посмотреть на метод invoke(), то мы заметим, что метод onClick вызывается на переменной this.receiver. Поле receiver принадлежит классу Function0 и должно проинициализироваться переменной listener, но так как переменная listener является lateinit переменной, то перед инициализацией receiver-а происходит проверка переменной listener на null и выброс ошибки, так как она пока не инициализирована. Поэтому наша программа завершается с ошибкой.

Button var10001 = new Button;Function0 var10003 = new Function0() {   public final void invoke() {      ((ButtonClickListener)this.receiver).onClick();   }};ButtonClickListener var10005 = this.listener;if (var10005 == null) {   Intrinsics.throwUninitializedPropertyAccessException("listener");}var10003.<init>(var10005);var10001.<init>((Function0)var10003);this.button = var10001;

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

Отсюда вытекает следующая интересная задача: Что напечатается после запуска программы?

class Button(    private val onClick: () -> Unit) {    fun performClick() = onClick()}class ButtonClickListener(    private val name: String) {    fun onClick() {        print(name)    }}class ScreenView {    var listener = ButtonClickListener("First")    val buttonLambda = Button { listener.onClick() }    val buttonReference = Button(listener::onClick)}fun main() {    val screenView = ScreenView()    screenView.listener = ButtonClickListener("Second")    screenView.buttonLambda.performClick()    screenView.buttonReference.performClick()}
  1. FirstFirst

  2. FirstSecond

  3. SecondFirst

  4. SecondSecond

Ответ

3

Спасибо за прочтение, надеюсь кому-то было интересно и полезно!

Подробнее..

Перевод Самая лучшая практика работа с path в Python

02.02.2021 02:11:38 | Автор: admin

Все та же проблема: список папок и дисков

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

Теперь я подниму планку и покажу, как можно было сделать лучше.

Объединяем пути с помощью Pathlib

Старые идеи в новом обличье?

Предыдущее решение с соединением путей выглядело следующим образом:

path_file = os.sep.join([path_dir, filename])

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

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

path_dir: str = r"C:/Users/sselt/Documents/blog_demo/"  # abschlieender Trennerfilename: str = "some_file"path_file = os.sep.join([path_dir, filename])# C:/Users/sselt/Documents/blog_demo/\some_file

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

В Python 3.4 появилось лучшее решение модуль pathlib. Он обрабатывает функции файлов и папок модуля os с помощью объектно-ориентированного подхода.

Напомню, старый вариант выглядел вот так:

import ospath = "C:/Users/sselt/Documents/blog_demo/"os.path.isdir(path)os.path.isfile(path)os.path.getsize(path)

А вот альтернативный:

from pathlib import Pathpath: Path = Path("C:/Users/sselt/Documents/blog_demo/")path.is_dir()path.is_file()path.stat().st_size

Оба варианта дают один и тот же результат. Так чем же второй вариант лучше?

Объектно-ориентированный и более устойчивый к ошибкам

Вызовы в основном являются объектно-ориентированными, нравится вам это или нет, но лично мне такой подход по душе. Здесь у нас есть такой объект, как определение path, у которого есть атрибуты и методы.

Однако пример с операторами перегрузки в данном случае более интересен:

filename: Path = Path("some_file.txt")path: Path = Path("C:/Users/sselt/Documents/blog_demo")print( path / filename )# C:\Users\sselt\Documents\blog_demo\some_file.txt

Сначала разделение на два пути кажется недопустимым. Однако объект path был перегружен так, чтобы работать как объединенный путь.

В дополнение к этому синтаксическому сахару объекты path будут перехватывать другие типичные ошибки:

filename: Path = Path("some_file.txt")# hier path mit berflssigem Trenner am Schlusspath: Path = Path("C:/Users/sselt/Documents/blog_demo/")# hier path mit doppeltem Trennerpath: Path = Path("C:/Users/sselt/Documents/blog_demo//")# hier path vllig durcheinanderpath: Path = Path("C:\\Users/sselt\\Documents/blog_demo")  # hier ein wilder Mix# alle Varianten fhren zum selben Ergebnisprint(path/filename)# C:\Users\sselt\Documents\blog_demo\some_file.txt

Такой вариант не только приятнее, но и устойчивее к неправильным входным данным. В дополнение к другим преимуществам код также не привязан к определенной операционной системе. Он определяет только generic объект path, который объявляется в системе Windows как WindowsPath, а в Linux как PosixPath.

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

Обработка пути с помощью os.walk

В своей последней статье я использовал os.listdir, os.path.isdir и рекурсивную функцию для итерации по дереву путей и разграничения файлов и папок.

Но os.walk предлагает решение получше. Этот метод создает не список, а итератор, который можно вызывать построчно. В результате мы получим соответствующий путь к папке и список всех файлов по этому пути. Весь процесс происходит рекурсивно, поэтому вы получите все файлы одним вызовом.

Лучшее решение с os.walk и Pathlib

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

filesurvey = []for row in os.walk(path):   # row beinhaltet jeweils einen Ordnerinhalt    for filename in row[2]:  # row[2] ist ein tupel aus Dateinamen        full_path: Path = Path(row[0]) / Path(filename)   # row[0] ist der Ordnerpfad        filesurvey.append([path, filename, full_path.stat().st_mtime, full_path.stat().st_size])

Если вам удастся улучшить этот вариант, не постесняйтесь рассказать мне об этом. Я был бы рад вашим отзывам!

Первую часть статьи можно найти здесь.


Перевод статьи подготовлен в преддверии старта курса Python Developer. Basic.

Также приглашаем всех желающих принять участие в бесплатном демо-уроке курса на тему Три кита: map(), filter() и zip().

Можно ли писать код, требующий циклов, но без циклов? Может ли он быть быстрее, чем, если бы мы использовали циклы в Python? Для реализации задуманного понадобится знание слов "callback", "iterator" и "lambda". Если интересно присоединяйтесь!

Подробнее..

Перевод 12 примеров улучшения кода с помощью dataclass

10.02.2021 18:20:17 | Автор: admin

В рамках курса Python Developer. Basic подготовили для вас перевод полезного материала.

Также приглашаем всех желающих на
открытый вебинар по теме Три кита: map(), filter() и zip(). Можно ли писать код, требующий циклов, но без циклов? Можно. Может ли он быть быстрее, чем, если бы мы использовали циклы в Python? Может. Для реализации задуманного понадобится знание слов "callback", "iterator" и "lambda". Будет сложно, но интересно. Присоединяйтесь.


Мы добавляем алгоритмы кластеризации с помощью пакетов scikit-learn, Keras и других в пакет Photonai. На 12 примерах мы покажем, как @dataclass улучшает код на Python. Для этого мы используем код из пакета Photonai для Machine Learning.

Обновитесь до Python 3.7 или более поздней версии

Декоратор @dataclass был добавлен в Python 3.7. Можно использовать Python 3.7 из Docker-образа, добавив в файл следующие команды /.bashrc_profile или /bashrc.txt.

devdir='<path-to-projects>/photon/photonai/dockerSeasons/dev/'testdir='<path-to-projects>/photon/photonai/dockerSeasons/test/'echo $devdirecho $testdirexport testdirexport devdir#alias updev="(cd $devdir; docker-compose up) &"alias downdev="(cd $devdir; docker-compose down) &"alias builddev="(cd $devdir; docker-compose build) &"#alias uptest="(cd $testdir; docker-compose up) & "alias downtest="(cd $testdir; docker-compose down) &"alias buildtest="cd $testdir; docker-compose build) &"

Если вы не можете найти файл /bashrc.txt создайте его самостоятельно с помощью touch/bashrc.txt. (в случае MacOS или одной из разновидностей операционных систем Linux или Unix.)

Примечание: Не забудьте указать в качестве исходника /.bashrc_profile или /bashrc.txt, когда закончите их редактировать.

Здесь вы найдете более подробную информацию о реализации Docker, которой я пользуюсь.

Примечание: вы можете добавить код Docker в свой проект из клонируемого репозитория на GitHub.

Добавьте подсказки типов

Python язык с динамической типизацией. В версиях Python от 3.5 есть подсказки типов (PEP 484). Я подчеркиваю, что именно подсказки, поскольку они не влияют на работу интерпретатора Python. Насколько вам известно, интерпретатор Python вообще их игнорирует.

Подсказки типов (обратите внимание, не строгая проверка типов) позволяют искать ошибки, находить дыры в безопасности и статически проверять типы после первого выполнения и при модульном тестировании.

В Python 3.7 подсказки типов нужны для полей в определении класса при использовании декоратора @dataclass.

Я добавляю подсказки типов во все приведенные примеры @dataclass. Если вы хотите узнать о них больше, рекомендую почитать:

  1. https://medium.com/swlh/future-proof-your-python-code-20ef2b75e9f5

  2. https://realpython.com/python-type-checking/

  3. https://docs.python.org/3/library/typing.html

Декоратор @dataclass уменьшает шаблонность

@dataclass был добавлен в Python 3.7. Основной движущей силой было желание избавиться от шаблонности, связанной с состоянием в определении класса def.

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

Примечание: Если вы не используете pandas, можно ускорить выполнение этих функции, с помощью быстрой вставки @jit из пакета numba.

@dataclass декорирует определение класса def и автоматически генерирует 5 методов init(), repr(), str, eq(), и hash().

Примечание: он генерирует и другие методы, но об этом позже.

Обратите внимание, что все эти 5 методов работают непосредственно с инкапсуляцией состояния класса. @dataclass практические полностью убирает повторяющийся шаблонный код, необходимый для определения базового класса.

Пример коротенького класса в photon/photonai/base/hyperpipe.py, декорированный с помощью @dataclass.

### Example #1class Data:    def __init__(self, X=None, y=None, kwargs=None):        self.X = X        self.y = y        self.kwargs = kwargs

Пример 1, после декорации =>

from dataclasses import dataclassfrom typing import Dictimport numpy as np@dataclassclass Data:    X: np.ndarray = None  # The field declaration: X    y: np.array = None    # The field declaration: y    kwargs: Dict = None   # The field declaration: kwargs

Примечание: Если тип не является частью объявления, то поле игнорируется. Используйте тип any для подстановки типа, если он меняется или во время выполнения неизвестен.

Сгенерировался ли код eq()?

### Example #2data1 = Data()data2 = Data()data1 == data1

Пример 2, вывод =>

True

Да! А что насчет методов repr() и str?

### Example #3print(data1)data1

Пример , вывод =>

Data(X=None, y=None, kwargs=None)Data(X=None, y=None, kwargs=None)

Да! А методы hash() и init?

Example #4@dataclass(unsafe_hash=True)class Data:    X: np.ndarray = None    y: np.array = None    kwargs: Dict = None        data3 = Data(1,2,3){data3:1}

Пример 4, вывод =>

{Data(X=1, y=2, kwargs=3): 1}

Да!

Примечание: У сгенерированного метода init все еще сигнатура (X, y, kwargs). Кроме того, обратите внимание, что подсказки типов были проигнорированы интерпретатором Python 3.7.

Примечание: У init(), repr(), strи eq() значение ключевого слова по умолчанию True, тогда как у hash() по умолчанию False.

Вы можете использовать inspect также, как и для любого другого экземпляра.

### Example #5from inspect import signatureprint(signature(data3.__init__))

Пример 5, вывод =>

(X: numpy.ndarray = None, y: <built-in function array> = None, kwargs: Dict = None) -> None

Круто!

Более длинный пример из photon/photonai/base/hyperpipe.py

### Example #6class CrossValidation:    def __init__(self, inner_cv, outer_cv,                 eval_final_performance, test_size,                 calculate_metrics_per_fold,                 calculate_metrics_across_folds):        self.inner_cv = inner_cv        self.outer_cv = outer_cv        self.eval_final_performance = eval_final_performance        self.test_size = test_size        self.calculate_metrics_per_fold = calculate_metrics_per_fold        self.calculate_metrics_across_folds =            calculate_metrics_across_folds        self.outer_folds = None        self.inner_folds = dict()Example #6 Output=>

Пример 6, после декорации =>

from dataclasses import dataclass@dataclassclass CrossValidation:    inner_cv: int    outer_cv: int    eval_final_performance: bool = True    test_size: float = 0.2    calculate_metrics_per_fold: bool = True    calculate_metrics_across_folds: bool = FalseNote:(Example #6) As any signature, keyword arguments fields with default values must be declared last.Note:(Example #6)  class CrossValidation: Readability has increased substantially by using @dataclass and type hinting.
### Example #7cv1 = CrossValidation()

Пример 7, вывод =>

TypeError: __init__() missing 2 required positional arguments: 'inner_cv' and 'outer_cv'Note:(Example #7) inner_cv and outer_cv are positional arguments. With any signature, you declare a non-default field after a default one. (Hint: If this were allowed, inheritance from a parent class breaks.)((Why? Goggle interview question #666.))
### Example #8cv1 = CrossValidation(1,2)cv2 = CrossValidation(1,2)cv3 = CrossValidation(3,2,test_size=0.5)print(cv1)cv3

Пример 8, вывод =>

CrossValidation(inner_cv=1, outer_cv=2, eval_final_performance=True, test_size=0.2, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)CrossValidation(inner_cv=3, outer_cv=2, eval_final_performance=True, test_size=0.5, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)
### Example #9cv1 == cv2

Пример 9, вывод =>

True
### Example #10cv1 == cv3

Пример 10, вывод =>

False
### Example #11from inspect import signatureprint(signature(cv3.__init__))cv3

Пример 11, вывод =>

(inner_cv: int, outer_cv: int, eval_final_performance: bool = True, test_size: float = 0.2, calculate_metrics_per_fold: bool = True, calculate_metrics_across_folds: bool = False) -> NoneCrossValidation(inner_cv=3, outer_cv=2, eval_final_performance=True, test_size=0.5, calculate_metrics_per_fold=True, calculate_metrics_across_folds=False)Note: (Example #11) The inspect function shows the signature of the class object while the__str__ default shows the instance state variables and their values.

Очень круто!

Упс, а что насчет:

self.outer_folds = Noneself.inner_folds = dict()

У нас есть переменные состояния, но они не создаются при вызове. Не волнуйтесь, @dataclass справится и с этим. Покажу в следующем разделе.

Обработка после инициализации

Существует такой метод, как post-init, который является частью определения @dataclass. Метод post_init выполняется после init, сгенерированного @dataclass. Он включает обработку после установки состояния сигнатуры.

Мы завершаем преобразование установив оставшееся состояние CrossValidation:

### Example 12from dataclasses import dataclass@dataclassclass CrossValidation:    inner_cv: int    outer_cv: int    eval_final_performance: bool = True    test_size: float = 0.2    calculate_metrics_per_fold: bool = True    calculate_metrics_across_folds: bool = False    def __post_init__(self):        self.outer_folds = None        self.inner_folds = dict()

Источники

Здесь вы найдете отличные варианты использования декоратора @dataclass:

  1. https://realpython.com/python-data-classes/

  2. https://blog.usejournal.com/new-buzzword-in-python-is-here-dataclasses-843dd1d372a5

Заключение

На 12 примерах до и после я показал, как @dataclass преобразует классы в пакете Photonai Machine Learning. Мы видели, как @dataclass повысил производительность и читаемость кода.

Улучшение читаемости упрощает понимание кода на продакшене для всех. Вы лучше понимаете результаты, тестируете, допускаете меньше ошибок и меньше тратитесь на техническое обслуживание.

Добавление @dataclass и подсказок типов демонстрирует, что Python продолжает расти и развиваться.

Примечание: Вы можете добавить обновленный код Photonai в свой проект из клонируемого репозитория на GitHub.

Я показал далеко не все возможности @dataclass. Поскольку мы только добавляем кластеризацию, я продолжу документировать изменения в photonai.


Узнать подробнее о курсе Python Developer. Basic.

Смотреть открытый вебинар по теме Три кита: map(), filter() и zip().

Подробнее..

CloudWatch и Lambda, или Как я перестал бояться и полюбил AWS

23.04.2021 14:18:12 | Автор: admin

Облачные провайдеры это реактор, где вместо обогащённого урана используется твой кошелёк. В позапрошлом году наша компания начала активно применять облака и мы в полной мере ощутили это на себе: несколько команд разрабатывали отдельные продукты, и для большинства тестов запускались виртуальные мощности в AWS. Мы с коллегами получили сертификаты от Amazon, и это, вместе с наглядностью происходящего, Free Tier и Soft Limitами, создавало ложное чувство спокойствия за свой бюджет. А когда этому чувству поддаёшься, получаешь локальный Чернобыль.

После нескольких превышений бюджета и встреч с начальством из-за забытых виртуалок мне надоело оправдываться, и я решил предотвратить лишние расходы, тем более что было известно, сколько можно потратить. Я использовал связку CloudWatch и Lambda и этот способ себя оправдал.

Почему настройка оповещений это плацебо

Первый способ, на который указывает сам AWS это настройка бюджетов AWS Budgets и оповещений из них. Да, здесь есть очевидные плюсы: это решение очень легко настроить, а сервис предупреждает в случае превышения текущего или прогнозируемого бюджета. С сентября 2020 г. он даже научился прогнозировать потребление и отправку событий в Amazon SNS.

Но объективно это просто ещё одна загорающаяся лампочка: оповещение может прийти на почту в середине ночи, а узнаёшь об этом только в понедельник утром. К тому же, по умолчанию AWS Budgets ничего не делает после того как заметил превышение. Он отлично подходит для контроля конкретных задач (например, отмеряет скорость расходования средств на проде), но среагирует слишком поздно, если у нас рядом будут работать запущенные по ошибке или безымянные виртуальные машины. Не подойдёт.

Как взять под контроль облачный ядерный реактор

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

Я решил, что нужно где-то разместить логику реагирования и дёргать за различные API в облаке. Проще всего это сделать в AWS Lambda, так как Serverless может работать бесплатно, логика реагирования оформляется как на NodeJS, так и на Python, а простота API-вызовов из облака и ролевая модель доступа сокращают время на тестирование такого решения.

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

Узнав на него ответ, можно принять решение: выключать инстанс за избыточностью или оставлять, потому что он нам по каким-то причинам нужен или пока не мешает.

Какие есть инструменты для контроля трат на AWS

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

  • Cost Explorer даёт подробную разбивку по сервисам, но единственное, что он показывает, это сколько аккаунт уже успел потратить. Ничего не знает про запущенные в данный момент виртуальные мощности, но с его помощью можно оптимизировать траты.

  • Budgets внутри Cost Explorer. С сентября 2020 г. он научился определять Usage в часах, но всё ещё не даёт ответа, как идентифицировать забытые сущности.

  • CloudTrail мощный инструмент для контроля всего, что происходит в облаке. С его помощью можно раскрывать все сущности, совершившие любое действие, и узнавать точное время. Главный его недостаток для мой задачи он собирает вообще всю информацию в массивные лог-файлы, без конкретного объяснения в какой момент он заканчивает один файл и начинает другой. Писать целый парсер среди множества файлов под одну задачу это как из пушки по воробьям.

  • CloudWatch отличный инструмент, чтобы мониторить состояние уже запущенных инстансов, но информация о запусках находится только в CloudTrail. Кроме сбора информации, умеет запускать события по расписанию.

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

Накопление данных Lambda + S3

Так как сложные варианты мне не подошли, я остановился на простом: решил накапливать знание о времени работы каждого инстанса самостоятельно, благо хранение файлов это тривиальная задача для Lambda + S3.

Для упрощения тестирования и читаемости кода разобьём задачу между несколькими Lambda:

  • отдельно собираем информацию по запущенным инстансам;

  • решаем, какие инстансы заслуживают выключения, а какие нет;

  • выключаем ненужные инстансы.

Оркестратором простой цепочки Lambda я сделал Step Functions, который может запускаться по расписанию из CloudWatch.

Пример решения

Файлы для Proof of Concept вы можете найти по ссылке на GitHub, а ниже я распишу, что делает этот код. В жизни он дорабатывался под нужды команд и использовался до тех пор, пока мы не обучили достаточное количество сотрудников соблюдать правила работы с облаком.

Что тут происходит?

Каждые 5 минут CloudWatch отправляет заготовленный вызов в Step Functions, который управляет последовательным запуском четырёх Lambda-функций. Каждая Lambda-функция исполняет код на JavaScript (версии Node.js 10.x), использует сервисы EC2, Config или S3, и завершает свою работу передачей JSON в следующую Lambda. Исполнение скрипта завершается записью логов в CloudWatch Logs.

Как это работает?

CloudWatch Event > Step Function > 4 Lambda functions > CloudWatch Logs

  1. Get List of Working Instances получает список работающих инстансов и передает его через JSON в следующую Lambda-функцию

  2. Update Budget Usage делает много вещей, но главное обновляет данные файла в S3-хранилище.

  3. Terminate Instances выключает инстансы, которые превышают бюджет или нас не устраивают.

Как устанавливать?

После размещения кода и настройки ролей в Lambda-функциях необходимо составить схему работы через Step Functions, а также привязать событие CloudWatch Event Rule, которое будет запускать систему каждые 5 минут.

Как бы я подошёл к этой проблеме сейчас

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

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

Если бы я сейчас начал заново решать проблему избыточного времени работы, то в S3 вместо файла бюджета оказался бы файл с договорённостями по использованию тегов. Lambda вместо оценки времени работы немедленно выключала бы все мощности, не подходящие по тегам. А контроль за расходами перешёл бы в связку AWS Budget и SNS с той же Lambda, управляющей их отключением.

Подробнее..

Из песочницы Функциональное программирование на Python для самых маленьких Часть 1 Lambda Функция

22.06.2020 10:10:48 | Автор: admin
image

Я решил написать эту серию статей ибо считаю что никто не должен сталкиваться с той стеной непонимания с которой столкнулся я где для того чтобы понять что то в Функциональном Программировании (Далее ФП) тебе надо уже знать многое в ФП. Эту статью я старался написать максимально просто настолько понятно чтобы ее суть мог уловить мой племянник школьник который сейчас делает свои первые шаги в Python

Небольшое введение


Для начала давайте разберемся что такое функциональное программирование, в чем его особенности, зачем оно было придумано и где и как его использовать. Стоп А зачем? Об этом написаны тонны материалов да и в этой статье судя по всему эта информация не особо нужна. Эта статья написана для того чтобы научились разбираться в коде который написан в функциональном стиле. Но если вы все таки хотите разобраться в истории Функционального Программирования и разобраться в том как оно работает под капотом то советую вам почитать о таких вещах как

  • Чистая Функция
  • Функции высшего порядка

Но для понимания того что сказано в этой статье знать это не обязательно.

Итак, начнем


Для начала надо понять следующее что такое Функциональное Программирование вообще. Лично я знаю две самые часто упоминаемые парадигмы в повседневном программировании это ООП и ФП.

Если упрощать совсем и объяснять на пальцах то описать эти две парадигмы можно следующим образом:

  • ООП это Объектно Ориентированное Программирование парадигма в которой пишут функции и методы аргументами которых являются Объекты каких то классов
  • По такой логике можно установить что ФП это парадигма в которой пишут функции и методы аргументами которых являются другие Функции все понятно. Ответ скрыт в самом названии

Как говорил мой любимый учитель zverok Виктор Шепелев вся работа в программировании это работа с данными взял какие то данные, поигрался с ними и вернул обратно.

Это относится и к ФП взял какие то данные, взял какую то функцию, поигрался с ними и выдал что то на выходе.

Не стану расписывать все ибо это будет оооочень долго цель данной статьи это помочь разобраться а не объяснить все и как что работает поэтому тут мы рассмотрим основные функции из ФП.

В большинстве своем ФП (как я его воспринимаю) это просто упрощенное написание кода. Любой код написанный в функциональном стиле может быть довольно легко переписан в обычном без потери качества но более примитивно. Цель ФП заключается в том чтобы писать код более простой, понятный и который легче поддерживать а также который занимает меньше памяти ну и куда же без этого разумеется главная новая мораль программирования DRY (Dont Repeat Yourself Не повторяйся).

Сейчас мы с вами разберем одну из основных функций которые применяются в ФП Lambda функцию.

В следующих статьях мы разберем такие функции как Map, Zip, Filter и Reduce.

Lambda функция


Lambda это инструмент в python и других языках программирования для вызова анонимных функций. Многим это скорее всего ничего не скажет и никак не прояснит того как она работает поэтому я расскажу вам просто механизм работы lambda выражений.

Все очень просто.

Рассмотрим пример. Например нам надо написать функцию которая бы считала площадь круга при известном радиусе.

Формула площади круга это

S = pi*(r**2)

где
S это площадь круга
pi математическая константа равная 3.14 которую мы получим из стандартной библиотеки Math
r радиус круга единственная переменная которую мы будем передавать нашей функции

Круг с радиусом

Теперь оформим это все в python

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно# Пишем функцию которая будет вычислять площадь круга по заданному радиусу в обычном варианте записиdef area_of_circle_simple(radius):  return pi_const*(radius**2)print(area_of_circle_simple(5))print(area_of_circle_simple(12))print(area_of_circle_simple(26))>>>78.5>>>452.16>>>2122.64

Вроде бы неплохо но это все может выглядеть куда круче если записывать это через lambda

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно# Пишем функцию которая будет вычислять площадь круга по заданному радиусу в функциональном стилеarea_of_circle_by_lambda = lambda radius: pi_const*(radius**2) print(area_of_circle_by_lambda(5))print(area_of_circle_by_lambda(12))print(area_of_circle_by_lambda(26))>>>78.5>>>452.16>>>2122.64

Для полноты картины покажем как вызывать lambda функцию анонимно

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака # после запятой иначе она будет выглядеть # как 3.141592653589793 а нам это будет неудобноprint((lambda radius: pi_const*(radius**2))(5))print((lambda radius: pi_const*(radius**2))(12))print((lambda radius: pi_const*(radius**2))(26))>>>78.5>>>452.16>>>2122.64

Что бы вы понимали анонимный вызов функции подразумевает то что вы используете её нигде не объявляя как в примере выше.

Лямбда функция работает по следующему принципу

func = lambda перечисляются аргументы через запятую : что то с ними делаетсяprint(func(передаем аргументы))>>>получаем результат того что находится после двоеточия двумя строками выше

Рассмотрим пример с двумя входными аргументами. Например нам надо посчитать объем конуса по следующей формуле:

V = (height*pi_const*(radius**2))/3

Конус с габаритами

Запишем это все в python:

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно#Формула объема конуса в классической форме записиdef cone_volume(height, radius):  volume = (height*pi_const*(radius**2))/3  return volumeprint(cone_volume(3, 10))>>>314.0

А теперь как это будет выглядеть в lambda форме:

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно#Формула объема конуса в лямбда записиcone_volume = lambda height, radius : (height*pi_const*(radius**2))/3print(cone_volume(3, 10))>>>314.0

Вообще количество переменных никак не ограничено. Для примера посчитаем объем усеченного конуса где у нас учитываются 3 разные переменные.

Объем усеченного конуса считается по формуле:

V = (pi_const*height*(r1**2 + r1*r2 + r2**2))/3

Усеченный конус с габаритами

И вот как это будет выглядеть в python классически

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобно#Формула объема усеченного конуса в классической записиdef cone_volume(h,r1,r2):  return (pi_const * h * (r1 ** 2 + r1 * r2 + r2 ** 2))/3print(cone_volume(12, 8, 5))print(cone_volume(15, 10, 6))print(cone_volume(20, 12, 9))>>>1620.24>>>3077.20>>>6970.8

А теперь покажем как это будет выглядеть в lambda но при этом не будем объявлять функцию заранее а опишем ее в момент вывода:

import math #Подключаем библиотеку mathpi_const = round(math.pi, 2) #округляем pi до второго знака после запятой иначе она будет выглядеть как 3.141592653589793 а нам это будет неудобноprint((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(12, 8, 5))print((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(15, 10, 6))print((lambda height, radius1, radius2 : (height*pi_const*(radius1**2 + radius1*radius2 + radius2**2))/3)(20, 12, 9))>>>1620.24>>>3077.20>>>6970.8

После того как мы разобрались как работает lambda функция давайте разберем еще кое что интересное что можно делать с помощью lambda функции что может оказаться для вас весьма неожиданным Сортировку

Сортировать одномерные списки в python с помощью lambda довольно глупо это будет выглядеть как бряцание мускулами там где оно совсем не нужно
Ну серьезно допустим у нас есть обычный список (не важно состоящий из строк или чисел) и нам надо его отсортировать тут же проще всего использовать встроенную функцию sorted()
И в правду, давайте посмотрим на это

new_int_list = [43,23,56,75,12,32] # Создаем список чиселprint(sorted(new_int_list)) # Сортируем список чиселnew_string_list = ['zum6z', 'yybt0', 'h1uwq', '2k9f9', 'hin9h', 'b0p0m'] # Создаем список строкprint(sorted(new_string_list)) # Сортируем список строк>>>[12, 23, 32, 43, 56, 75]>>>['2k9f9', 'b0p0m', 'h1uwq', 'hin9h', 'yybt0', 'zum6z']

В таких ситуациях действительно хватает обычного sorted() (ну или sort() если вам нужно изменить текущий список на месте без создания нового изменив исходный)

Но что если нужно отсортировать список словарей по разным ключам? Тут может быть запись как в классическом стиле так и в функциональном. Допустим у нас есть список книг вселенной Песни Льда и Пламени с датами их публикаций и количеством страниц в них.
Как всегда начнем с классической записи.

# Создали список из словарей книгasoiaf_books = [  {'title' : 'Game of Thrones', 'published' : '1996-08-01', 'pages': 694},  {'title' : 'Clash of Kings', 'published' : '1998-11-16', 'pages': 761},  {'title' : 'Storm of Swords', 'published' : '2000-08-08', 'pages': 973},  {'title' : 'Feast for Crows', 'published' : '2005-10-17', 'pages': 753},  {'title' : 'Dance with Dragons', 'published' : '2011-07-12', 'pages': 1016}]# Функция по получению названия книгиdef get_title(book):    return book.get('title')# Функция по получению даты публикации книгиdef get_publish_date(book):    return book.get('published')# Функция по получению количества страниц в книгеdef get_pages(book):    return book.get('pages')# Сортируем по названиюasoiaf_books.sort(key=get_title)for book in asoiaf_books:  print(book)print('-------------')# Сортируем по датамasoiaf_books.sort(key=get_publish_date)for book in asoiaf_books:  print(book)print('-------------')# Сортируем по количеству страницasoiaf_books.sort(key=get_pages)for book in asoiaf_books:  print(book)>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>------------->>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}>>>------------->>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}

А теперь перепишем это все через lambda функцию

# Создали список из словарей книгasoiaf_books = [  {'title' : 'Game of Thrones', 'published' : '1996-08-01', 'pages': 694},  {'title' : 'Clash of Kings', 'published' : '1998-11-16', 'pages': 761},  {'title' : 'Storm of Swords', 'published' : '2000-08-08', 'pages': 973},  {'title' : 'Feast for Crows', 'published' : '2005-10-17', 'pages': 753},  {'title' : 'Dance with Dragons', 'published' : '2011-07-12', 'pages': 1016}]# Сортируем по названиюasoiaf_books.sort(key=lambda book: book.get('title'))for book in asoiaf_books:  print(book)print('-------------')# Сортируем по датамasoiaf_books.sort(key=lambda book: book.get('published'))for book in asoiaf_books:  print(book)print('-------------')# Сортируем по количеству страницasoiaf_books.sort(key=lambda book: book.get('pages'))for book in asoiaf_books:  print(book)>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>------------->>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}>>>------------->>>{'title': 'Game of Thrones', 'published': '1996-08-01', 'pages': 694}>>>{'title': 'Feast for Crows', 'published': '2005-10-17', 'pages': 753}>>>{'title': 'Clash of Kings', 'published': '1998-11-16', 'pages': 761}>>>{'title': 'Storm of Swords', 'published': '2000-08-08', 'pages': 973}>>>{'title': 'Dance with Dragons', 'published': '2011-07-12', 'pages': 1016}

Таким образом lambda функция хорошо подходит для сортировки многомерных списков по разным параметрам.

Если вы повторите весь этот код самостоятельно написав его сами то я уверен что с этого момента вы сможете сказать что отныне вы понимаете как работают lambda выражения и сможете применять их в работе.

Но где же тут та самая экономия места, времени и памяти? Экономится максимум пара строк.

И вот тут мы подходим к реально интересным вещам.

Которые разберем в следующей статье где мы обсудим map функцию.
Подробнее..

Не практичный pythonпишем декоратор в однустроку

14.06.2021 16:14:45 | Автор: admin

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

Дисклеймер

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

Пролог

Идея написать декоратор в 4 строки меня изначально никак не трогала. Было просто интересно написать декоратор. Но в процессе спортивный интерес взял верх. Изначально все начиналось с простого кэширующего декоратора.

data = {}  # словарь для сохарения кэшируемых данныхdef decor_cache(func):      def wrapper(*args):        # создаем ключ из назвнии функции которую          # кэшируем и аргументов которые передаем        key = f"{func.__name__}{args}"         # проверяем кэшировли дунную функцию с аргументами         if args in data:            return data.get(key)        else:            # если кэшируем впервые            response = func(args) # запускаем функцию с аргументами             data[key] = response # кэшируем результат             return response      return wrapper

Сейчас задача из 18 строк кода, 11 если удалить пробелы и комментарии, сделать 4 строки. Первое что приходит на ум, записать конструкцию ifelse в одну строчку.

data = {}  # словарь для сохарения кэшируемых данныхdef decor_cache(func):      def wrapper(*args):        # создаем ключ из назвнии функции которую          # кэшируем и аргументов которые передаем        key = f"{func.__name__}{args}"        if not args in data            # если кэшируем впервые            response = func(args) # запускаем функцию с аргументами             data[key] = response # кэшируем результат         return data.get(key) if args in data else response         return wrapper

Теперь у нас 15 строк кода против 18, и появился ещё один if, что создает дополнительную вычислительную нагрузку, но сегодня мы собрались не для улучшению performance. Давайте добавим в этот мир энтропии и немного copy-paste и упростим переменную key.

data = {}  # словарь для сохарения кэшируемых данныхdef decor_cache(func):      def wrapper(*args):        if not args in data            # если кэшируем впервые            response = func(args) # запускаем функцию с аргументами             data[f"{func.__name__}{args}"] = response # кэшируем результат         return data.get(f"{func.__name__}{args}") if args in data else response         return wrapper

Теперь мы имеем 12 строк, без пробелов и комментариев 8 строк. Нам пока этого не достаточно, цель 4 строчки, и надо упростить ещё. Мы помним что декораторэто функция которая должна возвращать callable объект (функцию). Функцией может быть и lambda! Значит мы можем упростить и функцию wrapper и заменить её на lambdaанонимную функцию. И возвращать из функции "декоратора", анонимную функцию.

data = {}  # словарь для сохарения кэшируемых данныхdef decor_cache(func):  cache = labda *args: data.get(f"{func.__name__}{args}") if args in data else data[f"{func.__name__}{args}"] = func(args)     return labda *args: cache(*args) if cache(*args) else data.get(f"{func.__name__}{args}")

Цель достигнута! Декоратор в 4 строки, чистого кодаполучился. Как можно увидеть одной lambda функцией не обошлось, пришлось создать две lambda функцию. Первая lambda делает две вещи: если объект уже был закешировав возвращаем ранее закешировнанное значение и кэширует объект если он не был ранее кэширован но в таком случае мы нечего не возвращаем.

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

Конструкция получилась громоздкой, но все равно читаемая, более или менеепри условии если вы легко читаете lambda выражения. Много условностей, но это ещё не ад но мы к нему приближаемся. После всего проделанного пути все ещё кажется что можно ещё сократить количество строк кода. Мы уже вошли во вкус. Например те-же две lambda выражения можно совместить в одно выражение. Давайте объеденим две lambda функции в одну.

Для этого нам надо пойти на некоторое ухищрение, использовать тернарный оператор or. Тернарный оператор принимает два значения, справа и слева относительно себя, и пытается получить логический ответ True или False. Как оператор сравнения. Для того чтобы вычислить конструкцию слева и справа интерпретатор python выполнит код справа и слева. Слева у нас конструкция memory.update({f"{func.name}_{args[0]}": func(args[0])}) данное выражение вернет нам None метод update всегда будет возвращать нам None тернарный оператор воспримит этого как False и не будет это выводить, но главное что он выполнит этот код и мы обновим переменную memory. Справа у нас конструкция получения элемента по индексу из tupla, выражение простое и всегда будет давать результат, если в tuple будет запрашиваемый индекс.

data = {}  # словарь для сохарения кэшируемых данныхdef decor_cache(func):    return lambda *args: memory.get(f"{func.__name__}_{args[0]}") if f"{func.__name__}_{args[0]}" in memory else (lambda: memory.update({f"{func.__name__}_{args[0]}": func(args[0])}) or args[0])()

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

data = {}  # словарь для сохарения кэшируемых данныхdecor_cache = lambda func: lambda *args: memory.get(f"{func.__name__}_{args[0]}") if f"{func.__name__}_{args[0]}" in memory else (lambda: memory.update({f"{func.__name__}_{args[0]}": func(args[0])}) or args[0])()

Нарушая все паттерны, нам удалось создать кэширующий декоратор, почти в одну строку. Почти, потому что формально у нас есть строка объявления переменной data. Это мне не давало покоя... примерно 10 минут, пока не вспомнил что в python есть функция globals().

Функцияglobals()возвращает словарь с глобальной таблицей символов, определённых в модуле. По сути выдает словарь глобальных переменных (ключимя переменной, значениессылка на объект). Так мы получаем возможность создавать переменные в одно выражение, одной строкой. Давайте тогда для создания переменной с пустым словарем, будем использовать следующую конструкцию:

globals().update({memory: {}})

И для получения значения переменной конструкцию с get:

globals().get(memory)

После чего мы применили это на наш декоратор, и теперь действительно все получилось уместить в одну строку.

decor_cache = lambda func: lambda *args: globals().get("memory").get(f"{func.__name__}_{args[0]}") if f"{func.__name__}_{args[0]}" in globals().get("memory") else (lambda : globals().get("memory").update({f"{func.__name__}_{args[0]}": func(args[0])}) or args[0])()

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

Итоги

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

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

Подробнее..

Перевод Руководство по отладке бессерверных приложений

08.03.2021 12:14:02 | Автор: admin

Эволюция бессерверной архитектуры

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

После ряда успешных (и не очень) проектов по развертыванию фреймворков на корпоративных инфраструктурах и в облаке, была сформулирована концепция фреймворка FaaS (Function as a Service). Его задача обеспечить запуск приложений в контейнерах без сохранения состояния. Это дает разработчикам возможность сконцентрироваться на самом коде, а не на управлении сложной инфраструктурой и связанными с ней ресурсами. Это привело к изобретению бессерверной архитектуры, ориентированной исключительно на исполнение двоичных файлов приложений, при этом все необходимые ресурсы управляются сторонним провайдером и принадлежат ему. По своей сути бессерверная архитектура позволила предприятиям не только сильнее сосредоточиться на разработке основных приложений, но и существенно снизить накладные расходы.

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

Что такое отладка в бессерверной среде?

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

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

Различия в отладке бессерверных и монолитных приложений

На фоне того, что монолитные приложения постепенно теряют популярность и уступают бессерверным приложениям, с точки зрения простоты отладки они безусловно доминируют. Отладка монолитных приложений проводится локально, в их собственной интегрированной среде разработки (IDE), при этом разработчик создает точку останова на проверяемой строке кода. Поскольку IDE-системы поставляются уже с готовым режимом отладки это все, что требуется от разработчиков для выявления ошибок в коде. В бессерверной архитектуре, наоборот, наиболее популярным методом отладки является логирование. Разработчики отслеживают и записывают все действия сервиса в лог-файлы и таким образом получают представление о поведении приложения.

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

Кроме того, монолитные приложения поддерживают удаленную отладку. Вы можете подключить IDE к удаленным серверам, дав разработчикам привилегированный доступ. Несмотря на то, что это в принципе возможно, применять этот метод целесообразно только в тех случаях, когда локальное моделирование вряд ли даст результаты. Бессерверные приложения по природе своей не поддерживают удаленную отладку в силу отсутствия доступа на уровне ОС и сервера.

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

Типы отладочных фреймворков для бессерверных приложений

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

Таким образом, отладочные фреймворки для бессерверных приложений основываются на двух различных подходах с использованием AWS Lambda:

  1. Локальная отладка бессерверных приложений

  2. Отладка бессерверных приложений для облака

Локальная отладка бессерверных приложений

При разработке ПО важнейшая часть отладки происходит при локальном запуске. Модель бессерверных приложений (SAM) это решение, которое позволяет разработчикам запускать свои приложения локально на бессерверной платформе Amazon на базе AWS Lambda. В пакет решения входит два компонента: спецификация шаблона (SAM Template Specification) и интерфейс командной строки (SAM Command Line Interface). Спецификация шаблона позволяет определить приложение с помощью прав доступа, событий, API-интерфейсов и функций. При этом интерфейс командной строки позволяет вызывать функции локально, осуществлять пошаговую отладку функций, а также упаковывать и развертывать приложения в облаке. Здесь можно ознакомиться с полным руководством по использованию AWS SAM для локальной отладки на платформе AWS Lambda.

Отладка бессерверных приложений для облака

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

  • Первый этап тестирование API-шлюза. Вот некоторые из наиболее популярных инструментов для тестирования конечных точек API и шлюзов: SoapUI, Postman и Curl.

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

  • На следующем этапе изучается поведение приложения с помощью логов AWS CloudWatch. Функции AWS Lambda по умолчанию настроены на отправку логов в CloudWatch. Затем решение создает для каждой функции класс LogGroup, включающий все события, вызываемые этой функцией. Еще для отслеживания событий приложений можно использовать такие решения как Splunk и ELK.

  • На заключительном этапе вы можете провести анализ работы и отладку приложения с помощью AWS X-RAY. Это решение обеспечивает сквозной мониторинг событий и создает графическую визуализацию всех компонентов приложения. Именно эти возможности делают его идеальным инструментом отладки не только в процессе разработки приложений, но и в ходе промышленной эксплуатации.

Популярные средства отладки бессерверных приложений

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

Faasly.io

FaaSly дает комплексное представление обо всех ваших запросах как на уровне функций, так и на уровне вызовов, используя единую панель. Кроме того, он включает интерактивную карту сервиса (Service Map), выполняющую функцию диспетчерского центра и позволяющую быстро выявить узкие места в работе приложения и легко устранить их.

Встроенная система безопасности Благодаря глубокой интеграции с таким решением корпоративного класса как Envoy, можно достичь высокой степени защиты и соответствия нормативным требованиям благодаря возможности перехватывать каждый сетевой запрос, отправляемый из рабочего приложения AWS Lambda. Если ваши функции в какой-то момент будут скомпрометированы, ваши настоящие учетные данные AWS всегда останутся в безопасности.

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

AWS Cloudwatch

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

Инструменты SLS-Dev

Инструментарий Serverless Development Tools это открытый фреймворк с набором инструментов для разработчиков бессерверных платформ и приложений. Этот набор инструментов следует парадигме cloud-native, что способствует ускоренному развитию инноваций и их внедрению в промышленную эксплуатацию. Поскольку этот набор подразумевает оплату по мере использования, вы платите только за фактическое время работы и тестирования своего приложения на платформе. Притом что базовая версия включает почти весь критически важный функционал инструментария, расширенная версия под названием SLS-DevTools Guardian помогает оперативно обнаруживать проблемы по мере их возникновения и проводить отладку в режиме реального времени.

Lumigo

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

Dashbird

Если ваше приложение разработано на базе AWS Lambda, то Dashbird должен стать одним из ваших самых любимых инструментов отладки и оптимизации кода. Этот инструмент нацелен на обнаружение ряда распространенных сбоев в работе AWS Lambda, включая проблемы с памятью, тайм-ауты, ошибки времени выполнения, исключения и ошибки конфигурирования. Dashbird также оперативно предупредит вас о сбоях через Slack или по электронной почте и поможет сохранить непрерывную доступность сервиса. Помимо этого, можно интегрировать Dashbird с AWS X-RAY и получать информацию обо всех событиях и вызовах функций.

SignalFX

SignalFx это решение для облачного мониторинга, способное интегрироваться с основными поставщиками услуг, включая Google Cloud Functions, Azure Functions и AWS Lambda. Этот инструмент обеспечивает мониторинг производительности в режиме реального времени и непрерывную прозрачность для всех ваших бессерверных функций. Благодаря обширному списку функций, включающему метрики с малой задержкой, оптимизацию затрат, обнаружение холодного запуска и мониторинг времени выполнения, SignalFx неуклонно набирает популярность в сообществах разработчиков.

Заключительные мысли

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

В этой статье мы рассмотрели различные способы выполнения бессерверной отладки как при локальном внедрении, так и в облаке. Мы также рассмотрели ряд наиболее популярных и простых инструментов для отладки бессерверных приложений.

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

Подробнее..

Перевод Оптимизируем затраты с помощью AWS Cost Explorer

15.04.2021 14:13:15 | Автор: admin

У Amazon Web Services отличный бесплатный пакет:хороший набор сервисов и щедрая раздача кредитов для разработчиков. Я был уверен: проблем с оплатой моего окружения не будет, поэтому о расходах не беспокоился. Мое приложение на 100% serverless, и я всегда укладывался в уровень бесплатного использования, так что просто игнорировал вопрос оплаты. В какой-то момент я расслабился и потерял бдительность.

Постепенно мой продукт становился популярнее, пользователей стало больше и я получил счет на 62$. Пережить можно, но я задумался о темпах роста: моё приложение не было оптимизировано для уменьшения затрат, так как я об этом никогда не задумывался. Что ж, пришло время заняться сокращением расходов.

AWS Cost Explorer

Сервис AWS Billing dashboard хорошо подходит для оплаты счетов и показывает график прогноза счетов за текущий месяц. Но этот сервис едва ли претендует на звание лучшего в AWS. Месячный прогноз часто врет, поэтому лучше игнорировать его вовсе.

Помимо Billing Dashboard, соседний Cost Explorer. Он предоставляет очень хорошую детализацию и возможность прогнозирования. Кроме просмотра стандартной разбивки потребления в AWS, можно писать код под Cost Explorer, извлекая много ценной информации. И мне это дело зашло.

Используя Cost Explorer, я смог заранее определить уязвимые места и исправить их задолго до того, как с меня начнут списывать за них деньги. Еще раз спасибо AWS.

Пользовательский интерфейс

Прежде чем начать работать, надо познакомиться со стандартным видом консоли Billing Dashboard. Нужно сначала включить её, что будет стоить денег. Лучше сделать это заранее, чтобы потом не было мучительно больно. У кого много остатку, тот не боится недостатку!

Пользовательский интерфейс интуитивно понятный: здесь мы можем получать подробные отчеты по дням / услуге; или сгруппированные по времени / региону / услуге и множеством других способов.

Это мой график потраченного за последние несколько месяцев.

Отчеты

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

Бюджеты

Cost Explorer не ограничиваетсяотчетами и графиками. Чащеначинают осваивать AWS с небольших бюджетов и настроенных под них оповещений, но есть возможность сделать гораздо больше. Можно задать независимые бюджеты (по стоимости и использованию) для отдельных сервисов, и даже для отдельных инстансов или операций внутри сервиса.

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

Обнаружение аномалий

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

Cost Explorer API

Стандартный вид консоли управления меня устраивает но только для эпизодического ознакомления. Для того, чтобы получить нечто большее, AWS предоставляет отличный API. Репозиторий AWS Samples Github дает нам наглядный пример доступа к API Cost Explorer.

Мой код основан на этом примере, и позволяет разработать собственный отчет для Cost Explorera.

Код Lambda функции

import osimport sys# Required to load modules from vendored subfolder (for clean development env)sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "./vendored"))import boto3import datetimeimport loggingimport pandas as pd#For datefrom dateutil.relativedelta import relativedelta#For emailfrom email.mime.application import MIMEApplicationfrom email.mime.multipart import MIMEMultipartfrom email.mime.text import MIMETextfrom email.utils import COMMASPACE, formatdateSES_REGION="ap-south-1"CURRENT_MONTH = True#Default exclude support, as for Enterprise Support#as support billing is finalised later in month so skews trends    INC_SUPPORT = os.environ.get('INC_SUPPORT')if INC_SUPPORT == "true":    INC_SUPPORT = Trueelse:    INC_SUPPORT = FalseTAG_VALUE_FILTER = os.environ.get('TAG_VALUE_FILTER') or '*'TAG_KEY = os.environ.get('TAG_KEY')class CostExplorer:    """Retrieves BillingInfo checks from CostExplorer API    >>> costexplorer = CostExplorer()    >>> costexplorer.addReport(GroupBy=[{"Type": "DIMENSION","Key": "SERVICE"}])    >>> costexplorer.generateExcel()    """        def __init__(self, CurrentMonth=False):        #Array of reports ready to be output to Excel.        self.reports = []        self.client = boto3.client('ce', region_name='us-east-1')        # self.end = datetime.date.today().replace(day=1)        self.riend = datetime.date.today()        self.end = self.riend        # Default is last 12 months        self.start = (datetime.date.today() - relativedelta(months=+12)).replace(day=1) #1st day of month 12 months ago        self.ristart = (datetime.date.today() - relativedelta(months=+11)).replace(day=1) #1st day of month 11 months ago        self.sixmonth = (datetime.date.today() - relativedelta(months=+6)).replace(day=1) #1st day of month 6 months ago, so RI util has savings values        self.accounts = {}    def addRiReport(self, Name='RICoverage', Savings=False, PaymentOption='PARTIAL_UPFRONT', Service='Amazon Elastic Compute Cloud - Compute'): #Call with Savings True to get Utilization report in dollar savings        type = 'chart' #other option table        if Name == "RICoverage":            results = []            response = self.client.get_reservation_coverage(                TimePeriod={                    'Start': self.ristart.isoformat(),                    'End': self.riend.isoformat()                },                Granularity='MONTHLY'            )            results.extend(response['CoveragesByTime'])            while 'nextToken' in response:                nextToken = response['nextToken']                response = self.client.get_reservation_coverage(                    TimePeriod={                        'Start': self.ristart.isoformat(),                        'End': self.riend.isoformat()                    },                    Granularity='MONTHLY',                    NextPageToken=nextToken                )                results.extend(response['CoveragesByTime'])                if 'nextToken' in response:                    nextToken = response['nextToken']                else:                    nextToken = False            rows = []            for v in results:                row = {'date':v['TimePeriod']['Start']}                row.update({'Coverage%':float(v['Total']['CoverageHours']['CoverageHoursPercentage'])})                rows.append(row)              df = pd.DataFrame(rows)            df.set_index("date", inplace= True)            df = df.fillna(0.0)            df = df.T        elif Name in ['RIUtilization','RIUtilizationSavings']:            #Only Six month to support savings            results = []            response = self.client.get_reservation_utilization(                TimePeriod={                    'Start': self.sixmonth.isoformat(),                    'End': self.riend.isoformat()                },                Granularity='MONTHLY'            )            results.extend(response['UtilizationsByTime'])            while 'nextToken' in response:                nextToken = response['nextToken']                response = self.client.get_reservation_utilization(                    TimePeriod={                        'Start': self.sixmonth.isoformat(),                        'End': self.riend.isoformat()                    },                    Granularity='MONTHLY',                    NextPageToken=nextToken                )                results.extend(response['UtilizationsByTime'])                if 'nextToken' in response:                    nextToken = response['nextToken']                else:                    nextToken = False            rows = []            if results:                for v in results:                    row = {'date':v['TimePeriod']['Start']}                    if Savings:                        row.update({'Savings$':float(v['Total']['NetRISavings'])})                    else:                        row.update({'Utilization%':float(v['Total']['UtilizationPercentage'])})                    rows.append(row)                  df = pd.DataFrame(rows)                df.set_index("date", inplace= True)                df = df.fillna(0.0)                df = df.T                type = 'chart'            else:                df = pd.DataFrame(rows)                type = 'table' #Dont try chart empty result        elif Name == 'RIRecommendation':            results = []            response = self.client.get_reservation_purchase_recommendation(                #AccountId='string', May use for Linked view                LookbackPeriodInDays='SIXTY_DAYS',                TermInYears='ONE_YEAR',                PaymentOption=PaymentOption,                Service=Service            )            results.extend(response['Recommendations'])            while 'nextToken' in response:                nextToken = response['nextToken']                response = self.client.get_reservation_purchase_recommendation(                    #AccountId='string', May use for Linked view                    LookbackPeriodInDays='SIXTY_DAYS',                    TermInYears='ONE_YEAR',                    PaymentOption=PaymentOption,                    Service=Service,                    NextPageToken=nextToken                )                results.extend(response['Recommendations'])                if 'nextToken' in response:                    nextToken = response['nextToken']                else:                    nextToken = False            rows = []            for i in results:                for v in i['RecommendationDetails']:                    row = v['InstanceDetails'][list(v['InstanceDetails'].keys())[0]]                    row['Recommended']=v['RecommendedNumberOfInstancesToPurchase']                    row['Minimum']=v['MinimumNumberOfInstancesUsedPerHour']                    row['Maximum']=v['MaximumNumberOfInstancesUsedPerHour']                    row['Savings']=v['EstimatedMonthlySavingsAmount']                    row['OnDemand']=v['EstimatedMonthlyOnDemandCost']                    row['BreakEvenIn']=v['EstimatedBreakEvenInMonths']                    row['UpfrontCost']=v['UpfrontCost']                    row['MonthlyCost']=v['RecurringStandardMonthlyCost']                    rows.append(row)              df = pd.DataFrame(rows)            df = df.fillna(0.0)            type = 'table' #Dont try chart this        self.reports.append({'Name':Name,'Data':df, 'Type':type})    def addReport(self, Name="Default",GroupBy=[{"Type": "DIMENSION","Key": "SERVICE"},],     Style='Total', NoCredits=True, CreditsOnly=False, RefundOnly=False, UpfrontOnly=False, IncSupport=False):        type = 'chart' #other option table        results = []        if not NoCredits:            response = self.client.get_cost_and_usage(                TimePeriod={                    'Start': self.start.isoformat(),                    'End': self.end.isoformat()                },                Granularity='MONTHLY',                Metrics=[                    'UnblendedCost',                ],                GroupBy=GroupBy            )        else:            Filter = {"And": []}            Dimensions={"Not": {"Dimensions": {"Key": "RECORD_TYPE","Values": ["Credit", "Refund", "Upfront", "Support"]}}}            if INC_SUPPORT or IncSupport: #If global set for including support, we dont exclude it                Dimensions={"Not": {"Dimensions": {"Key": "RECORD_TYPE","Values": ["Credit", "Refund", "Upfront"]}}}            if CreditsOnly:                Dimensions={"Dimensions": {"Key": "RECORD_TYPE","Values": ["Credit",]}}            if RefundOnly:                Dimensions={"Dimensions": {"Key": "RECORD_TYPE","Values": ["Refund",]}}            if UpfrontOnly:                Dimensions={"Dimensions": {"Key": "RECORD_TYPE","Values": ["Upfront",]}}            tagValues = None            if TAG_KEY:                tagValues = self.client.get_tags(                    SearchString=TAG_VALUE_FILTER,                    TimePeriod = {                        'Start': self.start.isoformat(),                        'End': datetime.date.today().isoformat()                    },                    TagKey=TAG_KEY                )            if tagValues:                Filter["And"].append(Dimensions)                if len(tagValues["Tags"]) > 0:                    Tags = {"Tags": {"Key": TAG_KEY, "Values": tagValues["Tags"]}}                    Filter["And"].append(Tags)            else:                Filter = Dimensions.copy()            response = self.client.get_cost_and_usage(                TimePeriod={                    'Start': self.start.isoformat(),                    'End': self.end.isoformat()                },                Granularity='MONTHLY',                Metrics=[                    'UnblendedCost',                ],                GroupBy=GroupBy,                Filter=Filter            )        if response:            results.extend(response['ResultsByTime'])            while 'nextToken' in response:                nextToken = response['nextToken']                response = self.client.get_cost_and_usage(                    TimePeriod={                        'Start': self.start.isoformat(),                        'End': self.end.isoformat()                    },                    Granularity='MONTHLY',                    Metrics=[                        'UnblendedCost',                    ],                    GroupBy=GroupBy,                    NextPageToken=nextToken                )                results.extend(response['ResultsByTime'])                if 'nextToken' in response:                    nextToken = response['nextToken']                else:                    nextToken = False        rows = []        sort = ''        for v in results:            row = {'date':v['TimePeriod']['Start']}            sort = v['TimePeriod']['Start']            for i in v['Groups']:                key = i['Keys'][0]                if key in self.accounts:                    key = self.accounts[key][ACCOUNT_LABEL]                row.update({key:float(i['Metrics']['UnblendedCost']['Amount'])})             if not v['Groups']:                row.update({'Total':float(v['Total']['UnblendedCost']['Amount'])})            rows.append(row)          df = pd.DataFrame(rows)        df.set_index("date", inplace= True)        df = df.fillna(0.0)        if Style == 'Change':            dfc = df.copy()            lastindex = None            for index, row in df.iterrows():                if lastindex:                    for i in row.index:                        try:                            df.at[index,i] = dfc.at[index,i] - dfc.at[lastindex,i]                        except:                            logging.exception("Error")                            df.at[index,i] = 0                lastindex = index        df = df.T        df = df.sort_values(sort, ascending=False)        self.reports.append({'Name':Name,'Data':df, 'Type':type})    def generateExcel(self):        # Create a Pandas Excel writer using XlsxWriter as the engine.\        os.chdir('/tmp')        writer = pd.ExcelWriter('cost_explorer_report.xlsx', engine='xlsxwriter')        workbook = writer.book        for report in self.reports:            print(report['Name'],report['Type'])            report['Data'].to_excel(writer, sheet_name=report['Name'])            worksheet = writer.sheets[report['Name']]            if report['Type'] == 'chart':                # Create a chart object.                chart = workbook.add_chart({'type': 'column', 'subtype': 'stacked'})                chartend=13                for row_num in range(1, len(report['Data']) + 1):                    chart.add_series({                        'name':       [report['Name'], row_num, 0],                        'categories': [report['Name'], 0, 1, 0, chartend],                        'values':     [report['Name'], row_num, 1, row_num, chartend],                    })                chart.set_y_axis({'label_position': 'low'})                chart.set_x_axis({'label_position': 'low'})                worksheet.insert_chart('O2', chart, {'x_scale': 2.0, 'y_scale': 2.0})        writer.save()        #Time to deliver the file to S3        if os.environ.get('S3_BUCKET'):            s3 = boto3.client('s3')            s3.upload_file("cost_explorer_report.xlsx", os.environ.get('S3_BUCKET'), "cost_explorer_report.xlsx")        if os.environ.get('SES_SEND'):            #Email logic            msg = MIMEMultipart()            msg['From'] = os.environ.get('SES_FROM')            msg['To'] = COMMASPACE.join(os.environ.get('SES_SEND').split(","))            msg['Date'] = formatdate(localtime=True)            msg['Subject'] = "Cost Explorer Report"            text = "Find your Cost Explorer report attached\n\n"            msg.attach(MIMEText(text))            with open("cost_explorer_report.xlsx", "rb") as fil:                part = MIMEApplication(                    fil.read(),                    Name="cost_explorer_report.xlsx"                )            part['Content-Disposition'] = 'attachment; filename="%s"' % "cost_explorer_report.xlsx"            msg.attach(part)            #SES Sending            ses = boto3.client('ses', region_name=SES_REGION)            result = ses.send_raw_email(                Source=msg['From'],                Destinations=os.environ.get('SES_SEND').split(","),                RawMessage={'Data': msg.as_string()}            )     def lambda_handler(event, context):    costexplorer = CostExplorer(CurrentMonth=False)    #Default addReport has filter to remove Support / Credits / Refunds / UpfrontRI    #Overall Billing Reports    costexplorer.addReport(Name="Total", GroupBy=[],Style='Total',IncSupport=True)    costexplorer.addReport(Name="TotalChange", GroupBy=[],Style='Change')    costexplorer.addReport(Name="TotalInclCredits", GroupBy=[],Style='Total',NoCredits=False,IncSupport=True)    costexplorer.addReport(Name="TotalInclCreditsChange", GroupBy=[],Style='Change',NoCredits=False)    costexplorer.addReport(Name="Credits", GroupBy=[],Style='Total',CreditsOnly=True)    costexplorer.addReport(Name="Refunds", GroupBy=[],Style='Total',RefundOnly=True)    costexplorer.addReport(Name="RIUpfront", GroupBy=[],Style='Total',UpfrontOnly=True)    #GroupBy Reports    costexplorer.addReport(Name="Services", GroupBy=[{"Type": "DIMENSION","Key": "SERVICE"}],Style='Total',IncSupport=True)    costexplorer.addReport(Name="ServicesChange", GroupBy=[{"Type": "DIMENSION","Key": "SERVICE"}],Style='Change')    costexplorer.addReport(Name="Accounts", GroupBy=[{"Type": "DIMENSION","Key": "LINKED_ACCOUNT"}],Style='Total')    costexplorer.addReport(Name="AccountsChange", GroupBy=[{"Type": "DIMENSION","Key": "LINKED_ACCOUNT"}],Style='Change')    costexplorer.addReport(Name="Regions", GroupBy=[{"Type": "DIMENSION","Key": "REGION"}],Style='Total')    costexplorer.addReport(Name="RegionsChange", GroupBy=[{"Type": "DIMENSION","Key": "REGION"}],Style='Change')    if os.environ.get('COST_TAGS'): #Support for multiple/different Cost Allocation tags        for tagkey in os.environ.get('COST_TAGS').split(','):            tabname = tagkey.replace(":",".") #Remove special chars from Excel tabname            costexplorer.addReport(Name="{}".format(tabname)[:31], GroupBy=[{"Type": "TAG","Key": tagkey}],Style='Total')            costexplorer.addReport(Name="Change-{}".format(tabname)[:31], GroupBy=[{"Type": "TAG","Key": tagkey}],Style='Change')    #RI Reports    costexplorer.addRiReport(Name="RICoverage")    costexplorer.addRiReport(Name="RIUtilization")    costexplorer.addRiReport(Name="RIUtilizationSavings", Savings=True)    costexplorer.addRiReport(Name="RIRecommendation") #Service supported value(s): Amazon Elastic Compute Cloud - Compute, Amazon Relational Database Service    costexplorer.generateExcel()    return "Report Generated"

IAM Role

Чтобы запускаться, Lambda функция должна обладать ролью с приведенными ниже правами:

Базовая политика Lambda

{    "Version": "2012-10-17",    "Statement": [        {            "Effect": "Allow",            "Action": [                "logs:CreateLogGroup",                "logs:CreateLogStream",                "logs:PutLogEvents"            ],            "Resource": "*"        }    ]}

Разрешение для записи отчетов в S3 бакет

{    "Version": "2012-10-17",    "Statement": [        {            "Sid": "VisualEditor0",            "Effect": "Allow",            "Action": [                "s3:PutObject",                "s3:GetObject"            ],            "Resource": "arn:aws:s3:::account.admin/*"        }    ]}

Simple Email Service

{    "Version": "2012-10-17",    "Statement": [        {            "Sid": "VisualEditor0",            "Effect": "Allow",            "Action": [                "ses:SendEmail",                "ses:SendRawEmail"            ],            "Resource": "*"        }    ]}

Cost Explorer

{    "Version": "2012-10-17",    "Statement": [        {            "Sid": "VisualEditor0",            "Effect": "Allow",            "Action": "ce:*",            "Resource": "*"        }    ]}

Запуск на Event Bridge

Наконец, мы настраиваем регулярный запуск нашей Lambda функции на Event Bridge, например, 5 числа каждого месяца. В результате работы всех настроек я буду получать email с прикрепленным XLS-отчетом. Также можно настраивать срабатывание еженедельно и даже на определенные дни недели, при необходимости.

Подробнее..

Перевод Использование Slack для отслеживания очереди недоставленных сообщений SQS

28.12.2020 18:17:25 | Автор: admin
AWS SQS играет значительную роль в современной архитектуре приложений, особенно в бессерверной среде. При работе с SQS часто можно увидеть, что сообщения не были прочитаны; причиной могут быть ошибка в вашем коде, временное ограничение ресурсов, превышение бюджета API или зависимости в сообщениях, которые должны быть обработаны. В большинстве случаев вы хотели бы знать, что это за сообщения, если они много раз терпят неудачу, а затем узнать, почему, и устранить проблемы. Именно здесь в игру вступает очередь недоставленных сообщений SQS.





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

  1. Нет деталей о недоставленных сообщениях. CloudWatch только показывает, что есть сообщения в очереди недоставленных сообщений, не сообщая деталей, чтобы найти более подробную информацию, DevOps часто приходится использовать другие инструменты, например AWS CLI.
  2. Нет возможности воспроизвести недоставленные сообщения, то есть система не в состоянии вернуть недоставленное сообщение в SQS, по крайней мере это не так легко. Можно использовать AWS CLI, чтобы вернуть их обратно, но опять же это делает устранение уже неприятных неполадок еще более неприятным.

Вышеперечисленные проблемы могут быть решены с помощью Slack и Lambda, как показано ниже.

Очередь недоставленных сообщений SQS является триггером событий лямбда-функции, которая отправляет уведомления Slack, затем Slack передаёт действия пользователя обратно лямбда-функции и, наконец, помещает сообщения обратно в SQS.

Часть 1. Slack


Сначала нам нужно создать приложение в Slack, а затем написать приложение, чтобы оно принимало действия пользователя и взаимодействовало с Lambda.


Перейдите на api.slack.com, чтобы создать приложение, если у вас его ещё нет.

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



Создаём интерактивность, URL запроса это место, куда Slack посылает действия пользователя, конечная точка шлюза API функции Lambda.



Часть 2. Lambda


Я использую бессерверный фреймворк для управления лямбда-функциями. У нас будет две лямбда-функции:

  1. Функция мониторинга. Источник события функции очередь недоставленных сообщений SQS, поэтому, когда в очереди недоставленных сообщений появляется сообщение, эта функция активируется, а затем переадресует сообщение в Slack.
  2. Функция команды. Эта функция отвечает за прослушивание действий Slack, то есть нажатие кнопки отправляет сообщение обратно в исходную SQS.

Файл Serverless.yml показывает, как настраиваются эти две функции
service: slack-sqs-monitorframeworkVersion: "2.9.0"provider:  name: aws  versionFunctions: false  runtime: nodejs12.x  region: ap-southeast-2  iamRoleStatements:    - Effect: "Allow"      Action:        # You should only give least permissions to your functions.        - "sqs:*"      Resource:        - arn:aws:sqs:ap-southeast-2:xxxxxxxx:sqs.fifo # The original SQS arn        - arn:aws:sqs:ap-southeast-2:xxxxxxxx:deadletter.fifo # The dead letter queue arnplugins:  - serverless-webpack  - serverless-domain-managercustom:  customDomain:    rest:      domainName: labs.mianio.com      basePath: sqs-command      createRoute53Record: true      securityPolicy: tls_1_2  webpack:    webpackConfig: "webpack.config.js"    packager: "yarn"functions:  monitor:    handler: functions/monitor.handler    desciption: The function has the dead letter queue as the event, and forward the event to Slack    tags:      name: Monitor    environment:#     This is the webhook URL from the previous step      SLACK_ENDPOINT: https://hooks.slack.com/services/XXXXXXX/XXXXXX/XXXXXXXXX    events:      - sqs:#       Dead letter queue ARN          arn: arn:aws:sqs:ap-southeast-2:xxxxxxxx:deadletter.fifo  command:    handler: functions/command.handler    tags:      name: Command      desciption: The function handles Slack action and place the message back to the queue    environment:    # Credentials should be retrieved from Parameter Store       SLACK_SIGNING_SECRET: ${ssm:/deadletter/slack/signing-secret~true}      SLACK_OAUTH_TOKEN: ${ssm:/deadletter/slack/oauth-token~true}    events:      - http:          path: slack          method: post          cors: true


  • В функция мониторинга есть SLACK_ENDPOINT в качестве переменной окружения, которая будет использоваться для публикации в Slack.
  • Функция command находится за шлюзом API, конечная точка URL запроса для интерактивности Slack.

Функция декомпозирует события из очереди недоставленных сообщений и создаёт полезную нагрузку Slack для отправки. Смотрите api.slack.com/block-kit, чтобы узнать подробности о блоках для разработки в Slack.

Функция мониторинга
import middy from "@middy/core";import axios from "axios";import doNotWaitForEmptyEventLoop from "@middy/do-not-wait-for-empty-event-loop";export const monitor = async (event: any): Promise<any> => {  const records = event.Records;  await Promise.all(    records.map((record: any) => {      const messageGroupId = record?.attributes?.MessageGroupId;      const messageDeduplicationId = record?.attributes?.MessageDeduplicationId;      const approximateReceiveCount =        record?.attributes?.ApproximateReceiveCount;      return axios({        method: "post",        url: process.env.SLACK_ENDPOINT,        data: {          blocks: [            {              type: "section",              text: {                type: "mrkdwn",                text: `*Messsge ID*: ${record.messageId}`,              },            },            {              type: "section",              text: {                type: "mrkdwn",                text: `*Message Group Id*: ${messageGroupId}`,              },            },            {              type: "section",              text: {                type: "mrkdwn",                text: `*Message Deduplication Id*: ${messageDeduplicationId}`,              },            },            {              type: "section",              text: {                type: "mrkdwn",                text: `*Approximate Receive Count*: ${approximateReceiveCount}`,              },            },            {              type: "section",              text: {                type: "mrkdwn",                text: record.body,              },            },            {              type: "actions",              elements: [                {                  type: "button",                  style: "primary",                  text: {                    type: "plain_text",                    text: "Send back",                  },                  action_id: "sendback",                  value: record.body,                },              ],            },          ],        },        headers: {          "Content-type": "application/json; charset=utf-8",        },      });    })  );  return;};export const salesforceDeadLetterMonitor = middy(monitor).use(  doNotWaitForEmptyEventLoop());


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



Часть 3. Отправка обратно


Захватывающая деталь проекта это возможность отправить недоставленное сообщение обратно в SQS для переработки. При нажатии зеленой кнопки Send back Slack запускает POST-запрос на определённый ранее URL-адрес действия, то есть конечную точку шлюза API.

Функция command, которая отправляет недоставленное сообщение обратно в SQS
import { APIGatewayEvent } from "aws-lambda";import AWS from "aws-sdk";import qs from "qs";import axios from "axios";import middy from "@middy/core";import doNotWaitForEmptyEventLoop from "@middy/do-not-wait-for-empty-event-loop";import httpHeaderNormalizer from "@middy/http-header-normalizer";import httpEventNormalizer from "@middy/http-event-normalizer";import httpErrorHandler from "@middy/http-error-handler";import { slackVerifier } from "../../middlewares/slack/verify";const sqs = new AWS.SQS({ region: "ap-southeast-2" });const command = async (event: APIGatewayEvent) => {  if (!event.body) return { statusCode: 200 };  const requestBody: any = qs.parse(event.body);  const payload: any = JSON.parse(requestBody.payload);  let response;  const action = payload.actions[0];  if (action.action_id === "sendback") {    try {      const sqsPayload = payload.message.blocks.find(        (block: any) => block.block_id === "payload"      );      if (sqsPayload?.text?.text && action?.value) {        const payload = JSON.parse(sqsPayload.text.text);        await putBack(payload.jobName, payload.jobData, action.value);        response = {          payload: {            attachments: [              {                color: "good",                text: "Job was sent back",              },            ],            response_type: "in_channel",          },        };      }    } catch (error) {      console.error(error);    }  }  if (payload.response_url) {    await axios({      method: "post",      url: payload.response_url,      data: response.payload,      headers: {        "Content-type": "application/json; charset=utf-8",        Authorization: `Bearer ${process.env.SLACK_OAUTH_TOKEN}`,      },    });  } else if (response && !payload.response_url && response.payload) {    return {      body: JSON.stringify(response.payload),      statusCode: 200,    };  } else {    return {      statusCode: 200,    };  }};const putBack = async (name: string, data: any, workerUrl: string) => {  const params: any = {    MessageBody: JSON.stringify({ jobName: name, jobData: data }),    QueueUrl: workerUrl,  };  return new Promise((resolve: Function, reject: Function): any => {    sqs.sendMessage(params, (err: any, data: any): any => {      if (err) {        reject(err);      } else {        resolve(data);      }    });  });};export const handler = middy(command)  .use(doNotWaitForEmptyEventLoop())  .use(httpEventNormalizer())  .use(httpHeaderNormalizer())  .use(slackVerifier())  .use(httpErrorHandler());


Эта функция довольно проста:

  • Функция slackVerifier. Она проверяет, что POST-запрос направлен от Slack.

verifier.ts
import crypto from 'crypto';import qs from 'qs';export const slackVerifier = () => {  return {    before: async (handler: any) => {      const slackSignature =        handler.event.headers && handler.event.headers['x-slack-signature'];      const timestamp =        handler.event.headers &&        handler.event.headers['x-slack-request-timestamp'];      const time = Math.floor(new Date().getTime() / 1000);      if (Math.abs(time - timestamp) > 300) {        //  The request timestamp is more than five minutes from local time.        // It could be a replay attack, so let's ignore it.        return {          statusCode: 401,          body: JSON.stringify('Too old'),        };      }      const body = handler.event.body;      const sigBasestring = `v0:${timestamp}:${body}`;      const hash = crypto        .createHmac('sha256', process.env.SLACK_SIGNING_SECRET)        .update(sigBasestring, 'utf8')        .digest('hex');      const mySignature = `v0=${hash}`;      if (        !crypto.timingSafeEqual(          Buffer.from(mySignature, 'utf8'),          Buffer.from(slackSignature, 'utf8')        )      ) {        return {          statusCode: 401,          body: JSON.stringify('Invalid Signature'),        };      }      return;    },    onError: (handler: any) => {      return handler.callback(null, handler.error);    },  };};view raw

Переменная среды SLACKSIGNINGSECRET это переменная со страницы конфигурации Slack, которая вводится из определений бессерверной среды Serverless.yml.



Я настоятельно рекомендую хранить учётные данные в безопасном месте; извлекать их во время развертывания и держать вне исходного кода это хорошая практика.

SLACK_SIGNING_SECRET: ${ssm:/deadletter/slack/signing-secret~true}

  • Функция sendBack. Она получает полезную нагрузку от POST-запроса Slack и отправляет ее обратно в SQS.

Полезная нагрузка, которую Slack отправляет в command-функцию, выглядит так.



Она содержит response_url, который должен использоваться для отправки ответа обратно в Slack, чтобы подтвердить действие.



Код отправляет [полезную нагрузку ответа] обратно в Slack с помощью Bearer-токена.

await axios({    method: "post",    url: payload.response_url,    data: response.payload,    headers: {       "Content-type": "application/json; charset=utf-8",       Authorization: `Bearer ${process.env.SLACK_OAUTH_TOKEN}`    }});

SLACK_OAUTH_TOKEN вводится из переменных среды во время развёртывания. Вы можете получить её значение на странице конфигурирования Slack:



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

Задумали с нового года начать новую жизнь и подучиться? До конца этого года ещё можно ухватить курс с хорошей скидкой. А если использовать промокод HABR к скидке на баннере можно прибавлять еще 10%.

image



Подробнее..

И еще разок про Serverless

15.03.2021 14:07:31 | Автор: admin
Логотип AWS Lambda (ну или Half Life, я так и не понял)Логотип AWS Lambda (ну или Half Life, я так и не понял)

С публичного релиза AWS Lambda прошло ни много ни мало 6 с лишним лет. Реактивные функции, реагирующие на события, не только позволили по-другому смотреть на архитектуру систем и приложений, но и породили новый buzzword Serverless.

Мотивом к написанию этой статьи служит пост в блоге Netflix. Прочитав его, а так же комментарии русскоязычной аудитории, я прихожу к выводу, что направление "безсерверных вычислений" еще не обкатано вдоль и поперек (как и в англоязычной тусовке, чего греха таить), и неплохо бы проитерироваться по этой теме еще раз.

Мой пост будет опираться на технологии, разработанные в компании AWS, но тезисы из него применимы как к другим облачным провайдерам (GCP, Azure), так и к "домашним" имплементациям (OpenFaaS).


Я собираюсь пройтись по трем, на мой взгляд самым важным, пунктам, касающимся Serverless

  • Сначала я "демистифицирую" понятие Serverless, отделив его от реактивных функций Lambda,

  • Затем я пройдусь по архитектуре Lambda

  • И закончу этот пост рядом рекомендаций по разработке и сопровождению Serverless приложений.

В дальнейшем я буду использовать sls для обозначения Serverless. Я очень ленивый.

Lambda != Serverless, Lambda in Serverless == True

Горе павшим жертвами сладких речей адвокатов и евангелистов! "Вам не нужно будет управлять парком машин!", "Вы сможете развернуть свое приложение за считанные секунды!", "С помощью sls эффективность вашей разработки возрастет в разы!", - это и многое другое пытаются втолкнуть мне толпы героев и евангелистов AWS, но я познал суть.

Видите ли, риторика "мне не нужно думать об инфраструктуре" обречена на провал. Разве код работает в воздухе? Разве он не нуждается в сети? Разве мне нужно заботиться об инфраструктуре, если у меня есть Kubernetes, и я просто объявляю в нем сущности?

На мой взгляд, лучше всего преимущество sls описал Рик Хулихан на своем докладе посвященному архитектурным паттернам DynamoDB, кратко затронув новую парадигму (весь доклад сам по себе интересный, но ссылка ведет на последние несколько минут).

Fail cheap - вот, что конкурентно "продает" sls. Чтобы проверить тезис, мне не нужно так много ресурсов, как понадобилось бы чтобы развернуть небольшой кластер из виртуалок или контейнеров. Скорость здесь - не решающий фактор. В 2021-ом году, что Lambda функция, что таблица DynamoDB, что контейнер в ECS/EKS, что экземпляр EC2 - все это запускается в считанные минуты, если не секунды.

Скрытое послание 1

Если на этом моменте у вас пропал интерес, смело закрывайте статью. Serverless не нужен просто, чтобы был. Это иной вид мышления и разработки, и не сказать, что он лучше уже устоявшихся.

Что такое Serverless

AWS предоставляет огромный набор сервисов для построение систем различной степени нагрузки и тяжести, часть из которых имеет бирку sls - очереди и потоковая обработка (SQS, Kinesis), нотификации (SES, SNS), интеграции (EventBridge, Step Functions), хранилище (S3), базы данных (DynamoDB), работа с данными (Glue, Athena). Если вы смотрели мое выступление на HighLoad++, вы помните, как именно я отличаю sls от "обычных" сервисов. Sls создает для вас отдельный уровень абстракции, снимая с вас операционную нагрузку по работе с сервисом (на самом деле нет - теперь вам надо учить новую технологию/подход).

Взять к примеру базы данных DynamoDB - сама СУБД уже есть! В ваше пользование предоставляется таблица с ее "пропускной способностью" (WCU/RCU), индексы и прочие фичи. В случае с сервисом ETL Glue вас не допускают к работе с самим движком ETL - вместо этого вы объявлете схему трансформации данных, указываете источник и пункт назначения данных, а так же описываете задачу. Все остальное находится вне вашего ведения и управления, чтобы вы себе лишний раз ногу не отстрелили (если очень хотите отстрелить - поднимайте свое на виртуалках ЕС2).

Отличие будет в биллинге. Если в случае с базами RDS и задачами ECS Fargate где оплата идет поминутно (или посекундно?), с sls вы платите за объем (сколько гигабайт "весят" данные в S3) и утилизацию (запросы и трафик).

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

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

Ничего сложного, верно? Основное непонимание связано как раз с отсутствием простой истины в умах: sls - это абстракция над PaaS, которая тарифицируется по-другому.

Отсюда и сложности с sls приложениями. Sls приложение - не только набор Lambda функций за API GW. Sls приложение состоит только из sls сервисов.

Надеюсь, по этой теме вопросов у вас не осталось. Если остались - пишите в комментариях, отвечу всем по возможности.

Архитектура AWS Lambda

Родоначальница sls подхода вызвала в своей время немало шума, ведь ее релиз пришелся на то же время, что и буря вокруг Docker и рождение Kubernetes. За простотой Lambda функций (далее - Функция) стоит определенная хитрость, ведь мысль о том, что "просто нужно написать код и запустить" вызывает недоверие.

На самом же деле Функции несколько сложнее, чем звучат из сладких уст многочисленных sls евангелистов и героев, (и порождают не меньше сложностей, но об этом позже). Архитектура Функции состоит из 3 частей: Источник События (Triggering Event); внутренности AWS Lambda - runtime, deployment package (логика с зависимостями), слои (layers) и "дополнение" (extensions); точка назначения Функции (если имеется) - корзина S3, таблица DynamoDB, очередь SQS и т.д.

Картинка посвящается Ване Моисееву - моему другу и любителю иконок AWSКартинка посвящается Ване Моисееву - моему другу и любителю иконок AWS

В довесок к этому идут еще мониторинг, трассировка запроса и многие прелести, например Lambda Permission - а именно "разрешение" какому-то внешнему ресурсу (генератору событий) запускать Функции, передавая им полезную нагрузку (Событие).

Что происходит под капотом Lambda - история куда сложнее, и лучше послушать о ней из первоисточника.

Незамысловатая на первый взгляд архитектура породила очень много вопросов по эксплуатации и разработке, безопасности и мониторингу, compliance, governance и много много других. Покрыть их все - тема не одного десятка статей, поэтому дальше я пройдусь по основным практикам.

Как жить с Lambda

Давайте по порядку. Есть следующие проблемы, которые надо адресовать:

  1. Локальная разработка и отладка

  2. Архитектура sls приложений

  3. Организация кодовой базы

  4. Развертывание

  5. Эксплуатация (логирование, мониторинг, трассировка)

  6. Безопасность

Звучит сложно? Ну передавайте от меня привет тем, кто говорил, что sls это легко.

1. Локальная разработка и отладка

Стоит первым пунктом потому, что с этого все и начинается. AWS завез прекрасный инструмент под названием Serverless Application Model или SAM.

SAM представляет собой два компонента: интерфейс командной строки aws-sam-cli (ставится как через brew так и через pip) и шаблон (сильно упрощенный CloudFormation).

Для отладки Функции нужно объявить ее в шаблоне. Затем можно будет вызывать ее двумя способами: sam local invokeили sam local start-api(для Функций, отвечающих на вызовы API GW).

Если вы впервые видите SAM, то проще всего запустить один из quick start шаблонов, заботливо приготовленных для вас инженерами AWS.

$ sam initWhich template source would you like to use?1 - AWS Quick Start Templates2 - Custom Template LocationChoice: 1What package type would you like to use?1 - Zip (artifact is a zip uploaded to S3)2 - Image (artifact is an image uploaded to an ECR image repository)Package type: 1Which runtime would you like to use?1 - nodejs12.x2 - python3.83 - ruby2.74 - go1.x5 - java116 - dotnetcore3.17 - nodejs10.x8 - python3.79 - python3.610 - python2.711 - ruby2.512 - java8.al213 - java814 - dotnetcore2.1Runtime: 2Project name [sam-app]:Cloning app templates from https://github.com/aws/aws-sam-cli-app-templatesAWS quick start application templates:1 - Hello World Example2 - EventBridge Hello World3 - EventBridge App from scratch (100+ Event Schemas)4 - Step Functions Sample App (Stock Trader)5 - Elastic File System Sample AppTemplate selection: 1    -----------------------    Generating application:    -----------------------    Name: sam-app    Runtime: python3.8    Dependency Manager: pip    Application Template: hello-world    Output Directory: .    Next steps can be found in the README file at ./sam-app/README.md

Пройдя в sam-appможно вызвать функцию локально (первый запуск займет некоторое время, образ Lambda runtime нужно скачать):

$ sam local invoke HelloWorldFunctionInvoking app.lambda_handler (python3.8)Image was not found.Building image.......................................Skip pulling image and use local one: amazon/aws-sam-cli-emulation-image-python3.8:rapid-1.15.0.Mounting /Users/karentovmasyan/Development/Personal/dummy_project/sam-app/hello_world as /var/task:ro,delegated inside runtime containerSTART RequestId: e7036160-e11c-440a-b089-8099b1e0d500 Version: $LATESTEND RequestId: e7036160-e11c-440a-b089-8099b1e0d500REPORT RequestId: e7036160-e11c-440a-b089-8099b1e0d500Init Duration: 0.29 msDuration: 114.08 msBilled Duration: 200 msMemory Size: 128 MBMax Memory Used: 128 MB{"statusCode": 200, "body": "{\"message\": \"hello world\"}"}%

А установив плагин для JetBrain IDE или VS Code можно использовать и полноценный отладчик, чтобы узнать, где конкретно входящий JSON неправильно обрабатывается!

2. Архитектура sls приложений

Микросервисная архитектура подразумевает какой-то компонент (микросервис) достаточно изолированный от остальных, чтобы не рушить весь бизнес своим падением, и достаточно интегрированный, чтобы обслуживать конкретную бизнес-задачу.

Давайте представим некий market-service для биржи, который имплементирует API GET /markets. Вызовы на этот API вернут нам список текущих рынков, базовых валют, по прямому запросу - тикер.

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

GET /marketsGET /markets/baseGET /markets/ticker?pair=USD-RUB

В контейнерной оркестрации это делается с помощью обычного сервиса, а вся логика имплементирована в нем.

Реализовать такой функционал в Lambda тоже можно в одной функции. Полезная нагрузка в запросе будет выглядеть следующим образом (при условии, что запрос пришел из API GW):

# GET /markets{  # some params...  "resource": "/markets",  "path": "/markets",  "headers": #...  "queryStringParameters": null,  # etc...}# GET /markets/ticker?pair=USD-RUB{  # some params...  "resource": "/markets/ticker",  "path": "/markets/ticker",  "queryStringParameters": {  "pair": "USD-RUB"}} 

Мы можем осуществить проверку пути/ресурса и в зависимости от этого реализовать логику одной функции. Что-то навроде:

def get_markets():  passdef get_ticker(pair):  passdef handler(event, context):  path = event.get('path')  if path == '/markets':    return get_markets()  elif path == '/markets/ticker':    return get_ticker(event.get('queryStringParameters').get('pair'))

У этого подхода есть одно заметное достоинство - код централизован, и мы точно знаем где его править. Однако этот крайне неэффективен и является антипаттерном для sls архитектур. И вот ряд почему:

  1. Тарификация Функции идет по времени выполнения и мы тратим драгоценное время на избыточный control flow.

  2. Если Функция должна "ходить" в несколько мест (S3, DynamoDB и прочие API AWS), то мы даем ей опасно большое количество разрешений.

  3. Проблемы масштабируемости: нам нужно масштабировать GET /markets/ticker, но вот GET /marketsдорогой, и нам хотелось бы применить к нему throttling, что в условиях одной функции сделать невозможно.

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

И каждая функция будет иметь свою ограниченную область применения. Это порождает другую проблему - как нам организовать управление повторяемым кодом и кодовую базу в принципе?

3. Организация кодовой базы

Подход выше открывает 2 проблемы: управление общим кодом (что если 90% кода в Функциях одинаковые? Где моя бритва?!) и организация репозитория.

Раньше чтобы запаковать Функцию с зависимостями, нужно было создавать специальный deployment package и загружать его на S3. Что вручную, что с SAM это представляет собой следующее:

$ pip install -t . -r requirements.txt$ zip -r lambda.zip .$ aws s3 cp lambda.zip s3://bucket_name/

И если с внешними зависимостями еще можно как-то жить, то с внутренними возникает вопрос. А в "многофункциональном" sls приложении наверняка найдется одна или две библиотеки, которые делят между собой все функции.

Для компилируемых языков проблема не такая неприятная, управление зависимостями пройдет на стадии сборки. Для интерепретируемых языков, таких как Python, JavaScript и Ruby, проблема решается с помощью слоев (Lambda Layers). Слои Функций работают по схожему принципу со слоями контейнерных образов и предоставляют собой отдельное хранилище, монтируемое к runtime'у Функции.

Таким образом, проект можно организовать следующим образом:

$ tree -L 2. README.md  requirements.txt src # исходный код  lambda # handler'ы приложения  lib # общие библиотеки template.yaml tests venv

В шаблоне SAM можно указать директорию, чтобы создать нужный слой:

Resources:  Layer:    Type: AWS::Serverless::LayerVersion    Properties:      CompatibleRuntimes:          - python3.6          - python3.7          - python3.8          - python3.9      ContentUri: 'src/lib'

Ну и дальше дело техники - в ресурсе функции указать версию слоя, ссылаясь на нее в самом же шаблоне с помощью Fn::Ref.

Resources:  MarketsGet:    Type: AWS::Serverless::Function    Properties:      CodeUri: src/lambda/markets/get      Handler: markets_get.handler      Layers:        - !Ref Layer      Events:        ApiEvent:          Type: Api          Properties:            Path: /markets            RestApiId: !Ref Api            Method: get

Возникает вопрос: "А что делать, если в одном репозитории мне нужны разные версии зависимостей?" С помощью Слоев эта проблема не решается. Самое правильное - собрать, упаковать и положить зависимость в репозиторий. В случае с Python - Pip server.

4. Развертывание

С архитектурой и кодовой базой разобрались, теперь надо понять как "катить". Я уверен, ни для кого не станет сюрпризом, что катиться нужно с помощью IaC инструментов (молодое поколение называет это GitOps).

Что CloudFormation, что Terraform обладают возможностью объявлять ресурсы в облаке от Amazon, но объявление sls ресурсов может стать очень муторным процессом в виду гибкости и тонкой настройки.

SAM хорош тем, что создает абстракцию над CloudFormation для упрощенного объявления ресурсов sls приложений. В свою очередь Антон Бабенко сделал похожий инструмент для TF. Выбирать между одним или вторым я не буду (в тусовке амазонщиков у меня уже есть репутация "хейтера" Terraform), а разбор отличий заслуживает отдельной статьи.

Все еще живой Serverless framework тоже управляет жизненным циклом sls приложений (сам я с ним не работал, так что на ваш страх и риск).

Ну и вишенка на торте и вершина айсберга Хайпа - Cloud Development Kit или CDK. В отличие от выше описанных инструментов CDK объявляет инфраструктуру с использованием высокоуровневых языков программирования (TypeScript, JavaScript, Python, C#, Java), код которых компилируется ("синтезируется") в шаблон CloudFormation. Похожую историю имплементировали и ребята из HashiCorp, назвав ее cdktf.

Архитектура CDK/cdktf сама по себе сложная, но это не выступает барьером, который даже неопытные инженеры-облачники не в состоянии предолодеть. В сети полно материалов, как блогов, так и self-paced workshop, лекций, CDK day - их море.

Скрытое послание 2

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

В остальном развертывание sls приложений ничем не отличается от развертывания других приложений на AWS - у вас имеется цепочка поставки (pipeline), которая прогоняет тесты, пакует ресурсы и раскатывает их по различным регионам и аккаунтам. Делаете вы это с помощью sam deployили terraform apply- не так уж и важно.

5. Эксплуатация

С внутренними метриками и логами Функций проблем нет - они сами собой складываются в CloudWatch (разумеется, если вы не забыли прописать нужные политики в роли).

Отладить простейшую CRUD-функцию тоже много ума не надо. Прилетел JSON в Функцию, с ним что-то произошло, выхлоп лег куда-то. Если что-то в этом потоке пошло не так, достаточно локально отладить Функцию с помощью того же SAM CLI, приложив к ней багообразующий event.

Другое дело, если Функций много и работают они в каком-то долгом транзакционном процессе, наппример Step Functions. Отлавливать логи по времени, как прилетел злосчастный запрос, который не смог корректно обработать - то еще приключение, и разработчик может потратить неприемлемо много времени, чтобы найти баг.

На помощь придет инструмент observability от AWS под названием X-Ray. X-Ray является сервисом трассировки, он тесно интегрирован в sls экосистему. Достаточно включить поддержку X-Ray в Функции, и каждому запросу начнет присваиваться нужный идентификатор.

После этого можно спокойно посмотреть, куда прилетел запрос, через какие Функции прошел, и что было внутри логов тех Функций на момент принятия запроса. X-Ray стоит отдельных денег, но его можно как включить, так и выключить - что вполне себе удешевляет процесс отладки, предоставляя возможность на лету переводить ваше приложение в production в debug mode.

6. Безопасность

С очевидными вещами разбираться не будем. Всем известны такие базовые паттерны безопасности, как шифрование (at-rest, in-transit) и контроль сетевого периметра c помощью NACL и Security Groups.

У каждой Функции должна быть своя IAM роль. И по лучшим практикам безопасности каждая роль должна следовать least-privilege principle - то есть не даем больше прав, чем минимально нужно для работы приложения.

Иными словами, если наша Функция выполняет операцию dynamodb:PutItem, как минимум странно давать роли разрешение dynamodb:*(разрешаем все действия по отношению к DynamoDB). Так же странно видеть Resource: "*", в то время как работа идет в отношении одной таблицы.

Поэтому первое правило безопасности в sls приложениях - минимум прав. Потратье время и изучите как работает AWS IAM, и какие трюки там есть для тонкого управления доступом.

Скрытое послание 3

Для англоязычной аудитории я написал повесть в 5 томах про IAM и буду рад, если кто-то переведет ее на русский язык.

Второе правило - не разрешаем Функциям слишком много снова. Но на этот раз не только урезанием прав, но и ограничением области применения Функции. Каждая Функция выполняет одно атомарное и простое действие: регистрирует пользователя, авторизует пользователя, выводит список торговых пар на бирже, выводит цену пары, отправляет письмо и так далее.

Бояться тут надо не того, что кто-то взломает Функцию и изнутри нее будет делать всякие глупости (такая вероятность есть, но это тема другой статьи), но того, что роль Функции кто-то может assume, т.е. "принять на себя". Именно таким образом взломали Capital One: не взломали сервер, а просто получили ARN роли, через которую получили STS ключ, секрет и токен. Потому что кто-то ошибся в поле Principal. Стыд!


Вроде ничего не забыл? Я не ожидаю, что после прочтения вы сразу побежите писать Lambda функции и хвастаться стремительно сокращаюимся парком EC2 машин. Ниже я прилагаю ссылки, которые позволят вам двигаться дальше в изучении Serverless и Lambda в частности:

P.S. Я редко пишу на русском языке и еще реже чувствую страдания моей русскоязычной амазонской братии. Если у вас есть что-то, что хочется понять, но нет сил или навыка - пишите, я попробую разжевать это для вам в удобоваримом формате.

Подробнее..
Категории: Lambda , Amazon web services , Aws , Cloudnative , Serverless

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru