Разворачиваем AWS для разработки локально на базе LocalStack
Сейчас все больше компаний уходит в облака для запуска своих приложений. Мы в компании Namecheap не стали исключением и уже довольно долго используем сервисы AWS. В связи с этим перед нами встала задача упростить работу с сервисами AWS в условиях локальной разработки. Как приблизить локальное окружение к условиям прода?
В этой статье мы с вами поднимем небольшой проект, который будет взаимодействовать со стабами сервисов AWS, таких как: DynamoDB, SNS/SQS и S3.
Одним из самых распространённых решений для стабов сервисов AWS является LocalStack. Ранее этот проект разрабатывался Atlassian, но теперь брошен в дикий open-source и монетизируется за поддержку ряда дополнительных сервисов и саппорт.
TL; DR
- Поднимаем LocalStack при помощи docker-compose.
- Переключаем проект на эндпоинт сервиса 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:
- Переключаемся на использование HTTP-протокола (LocalStack из коробки работает через HTTP, хотя и поддерживает https).
- Устанавливаем 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 и попробовать взаимодействие с ним в тестовом проекте, прошу в репозиторий.