Niedawno miałem okazję współpracować z Amazon Web Services. Pozwoliło mi to połączyć wiedzę z praktycznym doświadczeniem. Co ważniejsze, dowiedziałem się również o narzędziu, które ułatwiło proces rozwoju.

Narzędziem tym jest LocalStack, a najlepiej opisuje je jego strona główna. "Zamiennik AWS w środowiskach programistycznych i testowych". Wyobraź sobie, że możesz emulować kluczowe usługi AWS lokalnie na swoim komputerze. Przyspiesza to cykle rozwoju i pozwala eksperymentować, uczyć się i testować. A wszystko to bez martwienia się o rozliczenia czy łączność z chmurą.

Czym jest LocalStack?

LocalStack może emulować usługi AWS. Umożliwia to rozwój bez polegania na usługach w chmurze. Obsługuje wiele usług, takich jak Lambda, S3, DynamoDB i inne. Zapewnia to elastyczność dla różnych przypadków użycia. Chociaż dostępna jest wersja płatna, darmowa wersja zawiera wiele kluczowych funkcji. Pełną listę można znaleźć na stronie z opisem funkcji LocalStack.

Instancją LocalStack można zarządzać za pomocą kilku metod:

  • LocalStack CLI: uruchamianie i zarządzanie kontenerem LocalStack z poziomu wiersza poleceń
  • Docker: użyj lokalnej instalacji Docker poprzez Docker CLI
  • Docker-Compose: zdefiniuj i uruchom LocalStack jako część pliku docker-compose
  • LocalStack Docker Extension: integruje się z Docker Desktop za pomocą dedykowanej wtyczki.
  • LocalStack Desktop: samodzielna aplikacja desktopowa do zarządzania LocalStack za pośrednictwem interfejsu użytkownika.
  • Helm: wdraża LocalStack w klastrze Kubernetes

Więcej informacji na temat rozpoczęcia można znaleźć tutaj.

Skupię się na zarządzaniu kontenerem LocalStack za pomocą docker-compose. Rozwiązanie będzie składać się z następującej kompozycji usług, funkcji Lambda, która po uruchomieniu prześle obiekt do wiadra S3 i zapisze informacje w tabeli DynamoDB.

W przypadku konfiguracji plik docker-compose wyglądałby następująco:

version: "3.8"

services:
  localstack:
    container_name: "demo-localstack"
    image: localstack/localstack
    ports:
      - "4566:4566" # Port that is used for LocalStack services emulation
    environment:
      # Defines the AWS services to emulate (defaults to all if not specified)
      - SERVICES=s3,lambda,dynamodb,iam
      # Configures default region and credentials for interacting with services
      - AWS_DEFAULT_REGION=eu-west-1
      - AWS_ACCESS_KEY_ID=test
      - AWS_SECRET_ACCESS_KEY=test
    volumes:
      # Mounts the Docker socket, necessary for some services like Lambda
      - "/var/run/docker.sock:/var/run/docker.sock"
      # Ensures that ./localstack directory content will
      # be executed and available on container startup
      - "./localstack:/etc/localstack/init/ready.d"

Kluczowe elementy, na które należy zwrócić uwagę:

  • Mapowanie portów: emulowane usługi są dostępne na porcie 4566.
  • Zmienne środowiskowe: definiują aktywne usługi AWS, domyślny region i poświadczenia używane przez emulację.
  • Wolumeny: gniazdo Docker jest wymagane dla funkcjonalności Lambda, podczas gdy gniazdo ./localstack umożliwia uruchamianie niestandardowych skryptów inicjalizacyjnych podczas uruchamiania.

Ta konfiguracja kładzie podwaliny, ułatwiając emulowanie usług AWS lokalnie. Kolejne kroki pokażą, jak wprowadzić tę konfigurację w życie.

Zarządzanie zasobami

Po uruchomieniu kontenera kolejnym krokiem jest skonfigurowanie zasobów. Podobnie jak w przypadku uruchamiania kontenera, istnieje kilka sposobów zarządzania tymi zasobami.

Wiersz poleceń

The AWS CLI działa z LocalStack. Jedyną potrzebną poprawką jest dodanie -endpoint-url wskazującego na kontener LocalStack. Na przykład, lista bucketów S3 może być wykonana z:

aws --endpoint-url=http://localhost:4566 s3api list-buckets

LocalStack zapewnia również poręczny wrapper dla AWS CLI o nazwie awslocal. Upraszcza on składnię i eliminuje potrzebę stosowania polecenia -endpoint-url parametr. To jest to samo polecenie przy użyciu awslocal:

awslocal s3api list-buckets

Z awslocalkonfiguracja zasobów jest prosta. Oto jak przygotować zasoby do tej konfiguracji:

Wiadro S3

awslocal s3api create-bucket \
    --bucket notes \
    --region eu-west-1 \
    --create-bucket-configuration LocationConstraint=eu-west-1

Tabela DynamoDB

awslocal dynamodb create-table \
    --table-name notes \
    --attribute-definitions AttributeName=file_name,AttributeType=S \
    --key-schema AttributeName=file_name,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST

Funkcja lambda

awslocal lambda create-function \
  --function-name notes-processor-lambda \
  --runtime python3.12 \
  --handler "notes_processor_lambda.lambda_handler" \
  --role arn:aws:iam::000000000000:role/admin \
  --zip-file "fileb:///etc/localstack/init/ready.d/notes_processor_lambda.zip"

Polecenia te można uruchamiać ręcznie, jednak wadą jest powtarzanie procesu za każdym razem. Aby zaoszczędzić czas, można to zautomatyzować za pomocą skryptu, jak wyjaśniono w następnej sekcji.

Uruchamianie skryptów inicjalizacyjnych w LocalStack

LocalStack pozwala nam uruchamiać niestandardowe skrypty podczas różnych faz jego cyklu życia: BOOT, START, READY i SHUTDOWN. Fazy te są udokumentowane na stronie referencyjnej haków inicjalizacyjnych.

W konfiguracji Docker Compose użyliśmy właściwości woluminu, która wskazuje na folder ./localstack katalog. Katalog ten zawiera skrypty, które LocalStack będzie uruchamiać podczas swojego cyklu życia. Zamontowany wolumin to:

- "./localstack:/etc/localstack/init/ready.d"

Umożliwia to zautomatyzowanie tworzenia zasobów, gdy kontener jest gotowy.

Poniżej znajduje się prosty skrypt inicjalizacyjny. Łączy on w sobie polecenia tworzenia bucketu S3, tabeli DynamoDB i funkcji Lambda. Jak również dodatkowe rejestrowanie w celu śledzenia procesu:

#!/bin/sh

echo "Creating S3 bucket."

awslocal s3api create-bucket \
    --bucket init-notes \
    --region eu-west-1 \
    --create-bucket-configuration LocationConstraint=eu-west-1

echo "Created notes S3 bucket."
echo "Creating notes DynamoDB table."

awslocal dynamodb create-table \
    --table-name init-notes \
    --attribute-definitions AttributeName=file_name,AttributeType=S \
    --key-schema AttributeName=file_name,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST

echo "Created notes DynamoDB table."
echo "Creating notes processing Lambda function."

awslocal lambda create-function \
  --function-name init-notes-processor-lambda \
  --runtime python3.12 \
  --handler "init_notes_processor_lambda.lambda_handler" \
  --role arn:aws:iam::000000000000:role/admin \
  --zip-file "fileb:///etc/localstack/init/ready.d/init_notes_processor_lambda.zip" \

echo "Created notes processing Lambda function."

Polecenia do utworzenia bucketu S3 i tabeli DynamoDB są proste. Tworzenie funkcji Lambda zawiera jednak dwa parametry, na które warto zwrócić uwagę:

  • -role arn:aws:iam::000000000000:role/admin: określa rolę IAM przypisaną do funkcji Lambda. W naszym przypadku jest to tylko wyśmiewana wartość (reguły IAM mogą być egzekwowane, ale tylko w wersji Pro).
  •   -zip-file "fileb:///etc/localstack/init/ready.d/init_notes_processor_lambda.zip": wskazuje na pakiet wdrożeniowy (plik .zip) zawierający kod funkcji Lambda i wszelkie zależności. Plik .zip jest przechowywany w katalogu ./localstack katalog. Jest on montowany w kontenerze.

Jako dodatkowy akcent, nazwy zasobów poprzedziłem przedrostkiem init-, aby było jasne, że są one częścią procesu inicjalizacji.

Konfigurowanie zasobów AWS za pomocą terraform

LocalStack ułatwia pracę z narzędziami IaC, takimi jak Terraform, Pulumi, CloudFormation i Ansible. W tej sekcji omówię , jak skonfigurować Terraform, aby utworzyć te same zasoby, które utworzyliśmy wcześniej. Kluczową częścią jest skonfigurowanie dostawcy AWS dla LocalStack. Następnie możemy napisać nasze definicje zasobów tak, jak zwykle za pomocą HashiCorp Configuration Language (HCL).

Oto prosty przykład tego, jak możemy zdefiniować nasze zasoby w Terraform:

provider "aws" {
  region                      = "eu-west-1"
  access_key                  = "test"
  secret_key                  = "test"
  s3_use_path_style           = true
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
  endpoints {
    s3       = "http://s3.localhost.localstack.cloud:4566"
    dynamodb = "http://localhost:4566"
    lambda   = "http://localhost:4566"
    iam      = "http://localhost:4566"
  }
}

resource "aws_s3_bucket" "terraform_notes_bucket" {
  bucket = "terraform-notes"
}

resource "aws_dynamodb_table" "terraform_notes_dynamodb_table" {
  name         = "terraform-notes"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "file_name"

  attribute {
    name = "file_name"
    type = "S"
  }
}

data "aws_iam_policy_document" "mock_lambda_role" {
  statement {
    effect = "Allow"

    principals {
      type = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }

    actions = ["sts:AssumeRole"]
  }
}

resource "aws_iam_role" "notes_processing_lambda_iam" {
  name               = "terraform_notes_processing_lambda_iam"
  assume_role_policy = data.aws_iam_policy_document.mock_lambda_role.json
}

resource "aws_lambda_function" "notes_processor_lambda" {
  function_name = "terraform_notes_processor_lambda"
  runtime       = "python3.12"
  handler       = "terraform_notes_processor_lambda.lambda_handler"
  role          = aws_iam_role.notes_processing_lambda_iam.arn
  filename      = "${path.module}/../terraform_notes_processor_lambda.zip"
}

Ta konfiguracja uruchomi zasobnik S3, tabelę DynamoDB i funkcję Lambda. Działa to tak samo jak w poprzednich przykładach. Jest jednak kilka rzeczy, na które należy zwrócić uwagę:

  • Role i polityki IAM dla lambda: rola i polityka dla funkcji Lambda są tutaj wyśmiewane. W prawdziwym środowisku AWS zdefiniowalibyśmy je bardziej rygorystycznie. Ale w przypadku LocalStack wystarczy to do utworzenia funkcji Lambda.
  • Zasoby prefiksów: Dodałem Terraform- do nazw zasobów, aby było jasne, że zostały one utworzone za pośrednictwem Terraform

Aby uzyskać więcej informacji na temat tworzenia zasobów AWS za pomocą Terraform, przeczytaj oficjalną dokumentację Terraform tutaj.

Jak przetestować konfigurację?

Po skonfigurowaniu zasobów nadszedł czas, aby sprawdzić, czy wszystko działa. Możemy sprawdzić stan zasobów za pomocą awslocalw interfejsie CLI LocalStack. Tutaj możemy potwierdzić, że emulowane usługi działają.

Wiadra S3

Aby sprawdzić, które buckety S3 zostały utworzone, możemy wyświetlić ich listę za pomocą poniższego polecenia:

awslocal s3api list-buckets

Oczekiwana odpowiedź

{
  "Buckets": [
  {
    "Name": "init-notes",
    "CreationDate": "2024-12-09T18:29:46.000Z"
  },
  {
    "Name": "terraform-notes",
    "CreationDate": "2024-12-09T18:31:07.000Z"
  },
  {
    "Name": "notes",
    "CreationDate": "2024-12-09T18:36:40.000Z"
  }
]
}

Tabele DynamoDB

Następnie, aby sprawdzić tabele DynamoDB, używamy następującego polecenia:

awslocal dynamodb list-tables

Oczekiwana odpowiedź:

{
  "TableNames": [
    "init-notes",
    "notes",
    "terraform-notes"
  ]
}

Funkcje lambda

I wreszcie, aby sprawdzić funkcje Lambda, możemy uruchomić polecenie:

awslocal lambda list-functions

Oczekiwana odpowiedź

{
  "Functions": [
    {
      "FunctionName": "init-notes-processor-lambda",
      "Runtime": "python3.12",
      "Handler": "init_notes_processor_lambda.lambda_handler"
	…
    },
    {
      "FunctionName": "terraform-notes-processor-lambda",
      "Runtime": "python3.12",
      "Handler": "terraform_notes_processor_lambda.lambda_handler"
	…
    },
    {
      "FunctionName": "notes-processor-lambda",
      "Runtime": "python3.12",
      "Handler": "notes_processor_lambda.lambda_handler"
	…
    }
  ]
}

Wywołanie funkcji Lambda

Jak wspomniano, nasza konfiguracja obejmuje funkcję Lambda zaprojektowaną do wykonywania dwóch zadań. Po pierwsze, umieszczanie obiektów w buckecie S3, a po drugie, przechowywanie informacji o obiekcie w tabeli DynamoDB. Aby to przetestować, wywołamy funkcję init-notes-processor-lambda funkcja. Kod, przygotowany specjalnie do tego zadania, wygląda następująco:

from datetime import datetime
import boto3

region = "eu-west-1"
access_key = "test"
secret_key = "test"
endpoint_url = "http://localstack:4566"

dynamodb = boto3.resource(
    "dynamodb",
    region_name=region,
    aws_access_key_id=access_key,
    aws_secret_access_key=secret_key,
    endpoint_url=endpoint_url
)

s3 = boto3.resource(
    "s3",
    region_name=region,
    aws_access_key_id=access_key,
    aws_secret_access_key=secret_key,
    endpoint_url=endpoint_url
)

def lambda_handler(event, context):
    file_name = event.get("fileName")
    file_content = event.get("content")

    bucket_name = "init-notes"
    file_key = f"{datetime.now()}/{file_name}"
    s3_object = s3.Object(bucket_name, file_key)
    s3_object.put(Body=file_content)

    table_name = "init-notes"
    notes_table = dynamodb.Table(table_name)
    note = {
        "file_name": file_name,
        "content": file_content
    }
    notes_table.put_item(Item=note)

Każdy klient używany w kodzie funkcji Lambda jest skonfigurowany do korzystania z adresu URL kontenera LocalStack. Aby przetestować funkcję, wywołujemy ją z przykładowym ładunkiem zawierającym nazwę i zawartość pliku:

awslocal lambda invoke \
  --function-name init-notes-processor-lambda \
  --payload '{"fileName": "example.txt", "content": "This is an example file content"}' \
  output.json

Oczekiwana odpowiedź funkcji Lambda:

{
  "StatusCode": 200,
  "ExecutedVersion": "$LATEST"
}

Kod stanu 200 wskazuje, że funkcja zakończyła się powodzeniem. Aby potwierdzić działanie funkcji, możemy sprawdzić wyniki, wyświetlając listę obiektów w zasobniku S3 i skanując tabelę DynamoDB.

Weryfikacja przesyłania S3

Możemy wyświetlić listę obiektów w init-notes S3, aby sprawdzić, czy plik został pomyślnie przesłany:

awslocal s3api list-objects --bucket init-notes

Oczekiwana odpowiedź listy obiektów:

{
  "Contents": [
    {
      "Key": "2024-12-09 19:13:25.644140/example.txt",
      "LastModified": "2024-12-09T19:13:25.000Z",
      "Size": 31
	…
    }
  ] 
}

Potwierdza to, że plik example.txt został pomyślnie przesłany do folderu init-notes bucket.

Weryfikacja wpisu DynamoDB

Na koniec, aby sprawdzić, czy metadane pliku zostały poprawnie zapisane w DynamoDB, skanujemy plik init-notes table:

awslocal dynamodb scan --table-name init-notes

Oczekiwana odpowiedź skanowania:

{
  "Items": [
    {
      "file_name": {
        "S": "example.txt"
      },
      "content": {
        "S": "This is an example file content"
      }
    }
  ] 
}

Potwierdza to, że nazwa i zawartość pliku example.txt zostały pomyślnie dodane do tabeli DynamoDB.

Interakcja z LocalStack z poziomu języka Java

Podobnie jak w przypadku Lambda, możemy zintegrować Javę w ten sam sposób. Aby to zrobić, należy użyć AWS SDK musi być skonfigurowany tak, aby wskazywał na punkt końcowy LocalStack. Korzystając z klientów usług AWS, takich jak te dla S3 i DynamoDB, możemy programowo wchodzić w interakcje z tymi zasobami.

Najpierw musimy dodać niezbędne zależności do naszego projektu:

//maven
<dependency>
    <groupId>software.amazon.awssdk</groupId>
    <artifactId>aws-sdk-java</artifactId>
    <version>2.28.7</version>
</dependency>

//gradle
implementation(‘software.amazon.awssdk:aws-sdk-java:2.28.7’)

Po uruchomieniu AWS SDK zapewnia dostęp do szerokiej gamy klientów usług. W tym przykładzie skupimy się na S3Client i DynamoDbClient. Klienci ci mogą być skonfigurowani do korzystania z punktu końcowego LocalStack. Dodatkowe ustawienia, takie jak region i dostęp/para kluczy tajnychnależy określić. Ta konfiguracja pozwala nam replikować proces weryfikacji wyników wywołania Lambda. Działa to w sposób podobny do metody, której używaliśmy wcześniej.

Oto przykład przy użyciu języka Java:

Region region = Region.EU_WEST_1;
String accessKeyId = "test";
String secretAccessKey = "test";
String endpointUrl = "http://localhost:4566";
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
        AwsBasicCredentials.create(accessKeyId, secretAccessKey));

S3Client s3Client = S3Client.builder()
        .region(region)
        .credentialsProvider(credentialsProvider)
        .endpointOverride(URI.create(endpointUrl))
        .serviceConfiguration(S3Configuration.builder()
                .pathStyleAccessEnabled(true)
                .build())
        .build();

DynamoDbClient dynamoDbClient = DynamoDbClient.builder()
        .region(region)
        .credentialsProvider(credentialsProvider)
        .endpointOverride(URI.create(endpointUrl))
        .build();

System.out.println("DynamoDB init-notes table items:");
dynamoDbClient.scan(ScanRequest.builder()
                .tableName("init-notes")
                .build()).items()
        .forEach(System.out::println);
System.out.println("S3 init-notes bucket contents:");
s3Client.listObjects(ListObjectsRequest.builder()
                .bucket("init-notes")
                .build()).contents()
        .forEach(System.out::println);

Uruchamiając ten kod, powinniśmy zobaczyć następujące dane wyjściowe w konsoli. Potwierdza to, że dane zarówno z S3, jak i DynamoDB są dostępne:

DynamoDB init-notes table items:
{file_name=AttributeValue(S=example.txt), content=AttributeValue(S=This is an example file content)}

S3 init-notes bucket contents:
S3Object(Key=2024-12-09 19:13:25.644140/example.txt, LastModified=2024-12-09T19:13:25Z)

Końcowe przemyślenia

Podsumowując, LocalStack to potężne narzędzie dla programistów poszukujących lokalnego środowiska do emulacji AWS. Oferuje różnorodne podejścia do zarządzania kontenerami i zasobami, od ręcznego używania poleceń po zautomatyzowane skrypty i narzędzia Infrastructure as Code (IaC), takie jak Terraform.

Integrując LocalStack z aplikacjami za pośrednictwem AWS SDKmożna łatwo wchodzić w interakcje z lokalnie emulowanymi zasobami AWS. Tworzy to idealne środowisko do testowania, uczenia się i eksperymentowania.

Każdy fragment kodu przedstawiony w tym artykule jest częścią projektu dostępnego w serwisie GitHub.

5/5 - (6 głosów)