Drive your career as React Developer with Symphony Solutions!
×Закрыть

Разворачиваем AWS для разработки локально на базе LocalStack

Сейчас все больше компаний уходит в облака для запуска своих приложений. Мы в компании Namecheap не стали исключением и уже довольно долго используем сервисы AWS. В связи с этим перед нами встала задача упростить работу с сервисами AWS в условиях локальной разработки. Как приблизить локальное окружение к условиям прода?

В этой статье мы с вами поднимем небольшой проект, который будет взаимодействовать со стабами сервисов AWS, таких как: DynamoDB, SNS/SQS и S3.

Одним из самых распространённых решений для стабов сервисов AWS является LocalStack. Ранее этот проект разрабатывался Atlassian, но теперь брошен в дикий open-source и монетизируется за поддержку ряда дополнительных сервисов и саппорт.

TL; DR

  1. Поднимаем LocalStack при помощи docker-compose.
  2. Переключаем проект на эндпоинт сервиса LocalStack.

Холодный старт на Windows

Самый простой путь развернуть LocalStack локально — запустить его при помощи Docker Compose.

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

В содержимое docker-compose-файла запишем такой код:

version: '2.1'
services:
localstack:
image: localstack/localstack:latest
ports:
- "4567-4584:4567-4584"
- "8080:8080"
volumes:
- "//var/run/docker.sock:/var/run/docker.sock"
environment:
- SERVICES=dynamodb
- PORT_WEB_UI=8080
- DOCKER_HOST=unix:///var/run/docker.sock

Осталось только поднять docker-compose-сервис:

docker-compose -f docker-compose.yml up -d localstack

Обратите внимание на установленную переменную окружения SERVICES. С ее помощью сейчас включён сервис DynamoDB. Чтобы включить другие сервисы, настроить Debug-трейсы и кое-что ещё, настоятельно рекомендую взглянуть в мануал.

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

docker: Error response from daemon: driver failed programming external connectivity on endpoint localstack_main (a156a7ce6d590937504c17b1f37f4634e7eaec09a9f8ba20cdf37b94424db39f): Error starting userland proxy: listen tcp 0.0.0.0:8080: bind: address already in use.

На одной из испытуемых систем это выглядело как-то так:

...
ports:
- "4567-4584:4567-4584"
- "9090:8080"
...
environment:
- PORT_WEB_UI=9090
...

Можно попробовать запустить LocalStack, как по мануалу — localstack start —docker. Но есть ряд минусов. Во-первых, вам придётся установить окружение Python, для того чтобы при помощи pip установить LocalStack. А во-вторых, вам понадобится либо установить докер, либо установить Java-окружение, для того чтобы заработали некоторые стабы сервисов.

Работа с DynamoDB

Итак, у нас уже запустился и работает LocalStack. Теперь мы можем проверить работоспособность и заодно подготовить сервисы, с которыми будем работать. Для настройки этих сервисов придётся использовать AWS CLI. Надеюсь, он уже у вас установлен. Для того, чтобы подключиться к нашим сервисам, нужно будет указать в конце команды кастомный эндпоинт при помощи следующего параметра —endpoint-url=http://localhost:4578, где номер порта мы можем взять из таблицы официального мануала.

Для начала проверим, что скажет LocalStack о состоянии таблиц:

aws dynamodb list-tables --endpoint-url=http://localhost:4569
{
    "TableNames": []
}

После чего создадим таблицу:

aws dynamodb create-table --table-name Todo \
--key-schema AttributeName=Id,KeyType=HASH --attribute-definitions AttributeName=Id,AttributeType=N AttributeName=Name,AttributeType=S \
--provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=5 --endpoint-url=http://localhost:4569 

И ещё раз взглянем в lLocalStack на список. Он покажет только что созданную таблицу:

aws dynamodb list-tables --endpoint-url=http://localhost:4569
{
    "TableNames": [
        "Todo"
    ]
}

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

HINT: Для тех, кому нужно сетапать инфраструкту не в ручном режиме, а с помощью терраформ, есть отличный механизм сделать это, задав маппинг ендпоинтов в модуле AWS:

provider "aws" {
    skip_credentials_validation = true
    skip_metadata_api_check = true
    s3_force_path_style = true
    access_key = "mock_access_key"
    secret_key = "mock_secret_key"
    endpoints {
        dynamodb = "http://localhost:4569"
    }
}

Чуть больше инфы по этому вопросу можно взять здесь.

Теперь давайте попробуем подключиться к DynamoDB из нашего тестового приложения. В любимой IDE создаём консольное dotnet core приложение. И сразу же устанавливаем пакет AWSSDK.DynamoDBv2. Общие правила для большинства подключения к сервисам LocalStack:

  1. Переключаемся на использование HTTP-протокола (LocalStack из коробки работает через HTTP, хотя и поддерживает https).
  2. Устанавливаем ServiceURL на порт этого стаба.

Давайте настроим подключение:

var clientConfig = new AmazonDynamoDBConfig()
{
    UseHttp = true,
    ServiceURL = "http://localhost:4569"
};

_dynamoClient = new AmazonDynamoDBClient(clientConfig);

После этого мы можем положить в нашу таблицу первое значение:

var putItemRequest = new PutItemRequest()
{
    TableName = TableName,
    Item = 
    {
        {
            "Id", new AttributeValue() { N = "42"}
        },
        {
            "Name", new AttributeValue() {S = "Get Up Early"}
        }
    }
};
await _dynamoClient.PutItemAsync(putItemRequest);

Можем проверить, что же сохранилось в LocalStack следующей командой:

aws dynamodb get-item --table-name Todo --key '{"Id":{"N":"42"}}' --endpoint-url=http://localhost:4569
{
    "Item": {
        "Id": {
            "N": "42"
        },
        "Name": {
            "S": "Get Up Early"
        }
    }
}

Добавляем SNS & SQS

Представим, что теперь нам нужно добавить SNS и SQS. Начнём с SNS. Для начала включим сервис и создадим топик. Для этого в compose-файле добавим в переменную окружения SERVICES разделённые запятой имена сервисов, как это сделано ниже:

...
environment:
- SERVICES=dynamodb,sns,sqs,s3
...

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

docker-compose -f docker-compose.yml restart localstack

В проект добавим nuget-пакеты AWSSDK.SimpleNotificationService и AWSSDK.SimpleNotificationService, для того чтобы получить возможность взаимодействовать с этими сервисами.

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

var snsConfig = new AmazonSimpleNotificationServiceConfig()
{
    UseHttp = true,
    ServiceURL = "http://localhost:4575"
};

snsClient = new AmazonSimpleNotificationServiceClient(snsConfig);

var sqsConfig = new AmazonSQSConfig()
{
    UseHttp = true,
    ServiceURL = "http://localhost:4576"
};

sqsClient = new AmazonSQSClient(sqsConfig);

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

private void CreateQueue()
{
    var queueCreationResult = await sqsClient.CreateQueueAsync("MyQueue");
    var queueUrl = queueCreationResult.QueueUrl;
    var topicCreationResult = await snsClient.CreateTopicAsync(new  CreateTopicRequest("TopicName"));
    var topicArn = topicCreationResult.TopicArn;
    var subscribeRequest = new SubscribeRequest(topicArn, "sqs", queueUrl);
    var subscribeResponse = await snsClient.SubscribeAsync(subscribeRequest);
}

В тестовых целях отправим в топик оповещение и вычитаем его из очереди:

...
// Publish message to topic
var request = new PublishRequest
{
    TopicArn = topicArn,
    Message = "Test Message"
};

await snsClient.PublishAsync(request);

...
// Read message from queue
var result = await sqsClient.ReceiveMessageAsync(queueUrl);
foreach (var message in result.Messages)
{
    Console.WriteLine(message.Body);
}
...

В консоли мы видим следующее:

{"MessageId": "e4e6ef59-107a-479d-952d-2a9b9e2da15c", "Type": "Notification", "Timestamp": "2019-10-05T13:27:36.397Z", "Message": "hello", "TopicArn": "arn:aws:sns:eu-west-3:000000000000:test"}

Это говорит о том, что всё успешно работает.

Сервис S3

Давайте примемся за самый используемый сервис — S3. Так как ранее мы его уже включили, можем оставить compose-файл в покое.

Устанавливаем nuget AWSSDK.S3 и создаём следующий конфиг для использования LocalStack-овского S3. Ничего нового — HTTP и кастомный порт, на котором крутится сервис:

var clientConfig = new AmazonS3Config()
{
    UseHttp = true,
    ServiceURL = "http://localhost:4572"
};
s3Client = new AmazonS3Client(clientConfig);

Давайте посмотрим, как этот сервис работает. Для этого создадим ведёрко и зальём на него файл.

await s3Client.PutBucketAsync(BucketName);
var putRequest = new PutObjectRequest()
{
    BucketName = BucketName,
    Metadata = { ["x-amz-meta-title"] = "Title" },
    FilePath = Path.GetFileName(FileName),
    ContentType = "text/plain"
};
await s3Client.PutObjectAsync(putRequest);

Можем взглянуть на его содержимое:

var result = await s3Client.ListObjectsAsync(BucketName);
foreach (var s3Object in result.S3Objects)
{
    Console.WriteLine(s3Object.Key);
}

Попробуем скачать:

using (GetObjectResponse response = await s3Client.GetObjectAsync(BucketName, FileName))
using (Stream responseStream = response.ResponseStream)
using (StreamReader reader = new StreamReader(responseStream))
{
    Console.WriteLine("Object metadata, Title: {0}", response.Metadata["x-amz-meta-title"]);
    Console.WriteLine("Content type: {0}", response.Headers["Content-Type"]);
    Console.WriteLine(reader.ReadToEnd());
}

Осталось только подчистить за собой состояние сервиса:

await s3Client.DeleteObjectAsync(BucketName, FileName);
await s3Client.DeleteBucketAsync(BucketName);

Выводы

Это всё! Вот так мы подключили приложение, настроили и поработали со стабами трёх сервисов AWS — DynamoDB, SNS/SQS и S3. Теперь, зная, как пользоваться этим инструментом, мы можем вести разработку приложения локально, а не реальный демо-аккаунт AWS. Это даёт нам возможность с самого начала разработки задать высокий уровень development experience. Всем, кому интересно чуть больше поиграться с LocalStack и попробовать взаимодействие с ним в тестовом проекте, прошу в репозиторий.

LinkedIn

11 комментариев

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

Не было ли у вас опыта использования более сложных AWS сервисов, например, Amazon EKS?

Опыта использования? Если Вы имеете в виду использование моков для EKS то нет, этого не приходилось делать.

Спасибо за статью, но почему вы используете очень старую версию Docker Compose 2.1? Текущая уже 3.7

Целью стьатьи было показать, что мок базовых сервисов это достаточно просто. Наверно потому, что не углублялся в версионность docker-compose файла.

Что там с лямбдами? Когда последний раз пробовал, можно было лямбде только на питона делать.

s3_force_path_style = true

Переключение на S3 path перестанет работать на AWS в конце сентября

aws.amazon.com/...​an-the-rest-of-the-story

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

Мы в компании Vonage, используем точно такой же подход для распиливания
c# монолита с почти что двадцатилетним стажем. Спасибо за внятное описание.

А как насчёт функционала автоматической подписи SNS топика на эндпоинт и продление подписки плюс сама процедура подписки (там топик должен сделать запрос дополнительно для подтверждения с валидацией респонза.. Процедура описана в документации)
PS
У меня на этом этапе отвалился localstack как способ замокать сервисы амазона для локальной разработки — увы оно сырое и полурабочее если рассматривать как замену реального аккаунта.. Возможно что изменилось но я года назад это все пробовал.. Не говоря про то что ещё пришлось лезть в исходники и фиксить некоторые баги что бы оно работало. (тоже нужен был S3 + SQS + SNS)

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

На счёт связки sns+sqs такой проблемы нет. Да и S3 покрыт не плохо.

Хотя странно, у меня всё восхитительно отрботало.

var subscribeRequest = new SubscribeRequest(topicArn, "http", "http://172.17.0.1:8080/post");
var subscribeResponse = await snsClient.SubscribeAsync(subscribeRequest);
var request = new PublishRequest
{
    TopicArn = topicArn,
    Message = "Test Message"
};

var result = await snsClient.PublishAsync(request);

Вот что видел мой тестовый эндпоинт:

--> POST /post HTTP/1.1
--> Host: 172.17.0.1:8080
--> User-Agent: Amazon Simple Notification Service Agent
--> Accept-Encoding: gzip, deflate
--> Accept: */*
--> Connection: keep-alive
--> Content-Type: text/plain
--> x-amz-sns-message-type: SubscriptionConfirmation
--> x-amz-sns-topic-arn: arn:aws:sns:us-east-1:000000000000:TopicName
--> x-amz-sns-subscription-arn: arn:aws:sns:us-east-1:000000000000:TopicName:86d632f6-0dc3-48e2-b9e4-473fb2a462c3
--> Content-Length: 533
--> 
--> {"MessageId": "0e876c8a-af80-482b-bc74-e6fbea11d55d", "Type": "SubscriptionConfirmation", "Timestamp": "2020-01-10T16:14:34.131362Z", "Message": "You have chosen to subscribe to the topic arn:aws:sns:us-east-1:000000000000:TopicName.\nTo confirm the subscription, visit the SubscribeURL included in this message.", "TopicArn": "arn:aws:sns:us-east-1:000000000000:TopicName", "Token": "0350b36c", "SubscribeURL": "http://localhost:4575/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:us-east-1:000000000000:TopicName&Token=0350b36c"}

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

Подписаться на комментарии