Собираем базу компаний, которые сокращают штат или зарплаты из-за кризиса. Заполните анкету
×Закрыть

Разворачиваем 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

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

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

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

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

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

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 и есть алгоритм валидации и подтверждения подписки

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