Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 10 additions & 20 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ jobs:
with:
node-version: '22'

- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'

- name: Set up JDK
uses: actions/setup-java@v2
with:
Expand All @@ -41,23 +36,18 @@ jobs:
- name: Set up Maven
run: sudo apt-get install -y maven

- name: Install lstk
run: npm install -g @localstack/lstk

- name: Start LocalStack
uses: LocalStack/setup-localstack@v0.2.4
with:
image-tag: 'latest'
use-pro: 'true'
configuration: LS_LOG=trace
install-awslocal: 'true'
env:
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
run: |
lstk start

- name: Install Terraform
uses: hashicorp/setup-terraform@v3

- name: Install tflocal
run: |
pip install --upgrade terraform-local

- name: Build project
run: |
cd shipment-picture-lambda-validator
Expand All @@ -66,9 +56,9 @@ jobs:
- name: Deploy using Terraform
run: |
cd terraform
tflocal init
tflocal plan
tflocal apply --auto-approve
lstk tf init
lstk tf plan
lstk tf apply --auto-approve

- name: Build frontend
run: |
Expand All @@ -79,11 +69,11 @@ jobs:
- name: Show LocalStack logs
if: always()
run: |
localstack logs
lstk logs

- name: Stop LocalStack
run: |
localstack stop
lstk stop

- name: Run Testcontainers tests
env:
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ src/main/shipment-list-frontend/npm-debug.log*
src/main/shipment-list-frontend/yarn-debug.log*
src/main/shipment-list-frontend/yarn-error.log*

src/main/resources/buckets.properties

# lstk
.lstk/lstk.log

# terraform
terraform/.terraform/
terraform/terraform.tfstate
Expand Down
13 changes: 13 additions & 0 deletions .lstk/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# lstk auto-discovers ./.lstk/config.toml (relative to cwd) before falling back to the
# per-user config, so `lstk start` in .github/workflows/ci.yml picks this up with no extra flag.
# Run 'lstk config path' to see where your personal (non-CI) config lives instead.

[[containers]]
type = "aws"
image = "localstack/localstack-pro"
tag = "latest"
port = "4566"
env = ["ci"]

[env.ci]
LS_LOG = "trace"
18 changes: 6 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ usage:
check:
@command -v docker > /dev/null 2>&1 || { echo "Docker is not installed. Please install Docker and try again."; exit 1; }
@command -v node > /dev/null 2>&1 || { echo "Node.js is not installed. Please install Node.js and try again."; exit 1; }
@command -v localstack > /dev/null 2>&1 || { echo "LocalStack is not installed. Please install LocalStack and try again."; exit 1; }
@command -v terraform > /dev/null 2>&1 || { echo "Terraform is not installed. Please install Terraform and try again."; exit 1; }
@command -v tflocal > /dev/null 2>&1 || { echo "tflocal is not installed. Please install tflocal and try again."; exit 1; }
@command -v lstk > /dev/null 2>&1 || { echo "lstk is not installed. Please install lstk and try again."; exit 1; }
@command -v mvn > /dev/null 2>&1 || { echo "Maven is not installed. Please install Maven and try again."; exit 1; }
@command -v java > /dev/null 2>&1 || { echo "Java is not installed. Please install Java and try again."; exit 1; }
@echo "All required prerequisites are available."
Expand All @@ -31,7 +30,7 @@ install:
## Deploy the infrastructure
deploy:
@echo "Deploying the infrastructure..."
cd terraform && tflocal init && tflocal plan && tflocal apply --auto-approve
cd terraform && lstk tf init && lstk tf plan && lstk tf apply --auto-approve
@echo "Infrastructure deployed successfully."

## Test the application
Expand All @@ -53,20 +52,15 @@ frontend:
## Start LocalStack in detached mode
start:
@test -n "${LOCALSTACK_AUTH_TOKEN}" || (echo "LOCALSTACK_AUTH_TOKEN is not set. Find your token at https://app.localstack.cloud/workspace/auth-token"; exit 1)
@LOCALSTACK_AUTH_TOKEN=$(LOCALSTACK_AUTH_TOKEN) localstack start -d
@LOCALSTACK_AUTH_TOKEN=$(LOCALSTACK_AUTH_TOKEN) lstk start

## Stop the Running LocalStack container
stop:
@echo
localstack stop

## Make sure the LocalStack container is up
ready:
@echo Waiting on the LocalStack container...
@localstack wait -t 30 && echo LocalStack is ready to use! || (echo Gave up waiting on LocalStack, exiting. && exit 1)
lstk stop

## Save the logs in a separate file
logs:
@localstack logs > logs.txt
@lstk logs > logs.txt

.PHONY: usage install check start ready deploy logs stop test
.PHONY: usage install check start deploy logs stop test
15 changes: 7 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ The following diagram shows the architecture that this sample application builds
## Prerequisites

- A valid [LocalStack for AWS license](https://localstack.cloud/pricing). Your license provides a [`LOCALSTACK_AUTH_TOKEN`](https://docs.localstack.cloud/getting-started/auth-token/) to activate LocalStack.
- [`localstack` CLI](https://docs.localstack.cloud/getting-started/installation/#localstack-cli).
- [AWS CLI](https://docs.localstack.cloud/user-guide/integrations/aws-cli/) with the [`awslocal` wrapper](https://docs.localstack.cloud/user-guide/integrations/aws-cli/#localstack-aws-cli-awslocal).
- [Terraform](https://docs.localstack.cloud/user-guide/integrations/terraform/) with the [`tflocal`](https://github.com/localstack/terraform-local) wrapper.
- [`lstk` CLI](https://docs.localstack.cloud/aws/tooling/lstk/).
- [AWS CLI](https://docs.localstack.cloud/user-guide/integrations/aws-cli/) with the [`lstk aws` proxy](https://docs.localstack.cloud/aws/tooling/lstk/).
- [Terraform](https://docs.localstack.cloud/user-guide/integrations/terraform/) with the [`lstk tf` proxy](https://docs.localstack.cloud/aws/tooling/lstk/).
- [Maven 3.8.5+](https://maven.apache.org/install.html) & [Java 17](https://www.java.com/en/download/help/download_options.html)
- [Node.js](https://nodejs.org/en/download/) & [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
- [`make`](https://www.gnu.org/software/make/) (**optional**, but recommended for running the sample application)
Expand Down Expand Up @@ -64,11 +64,10 @@ This will:

## Deployment

Start LocalStack with the `LOCALSTACK_AUTH_TOKEN` pre-configured:
Start LocalStack:

```shell
localstack auth set-token <your-auth-token>
localstack start
lstk start
```

To deploy the sample application, run the following command:
Expand Down Expand Up @@ -138,7 +137,7 @@ Available actions:

This sample demonstrates Infrastructure as Code (IaC) testing by using identical Terraform configurations for both AWS and LocalStack environments. The application leverages Spring profiles to seamlessly switch between production and development configurations without code changes.

The Terraform configuration defines all necessary AWS resources and their relationships, while `tflocal` automatically reconfigures endpoints for LocalStack. This approach enables:
The Terraform configuration defines all necessary AWS resources and their relationships, while `lstk tf` automatically reconfigures endpoints for LocalStack. This approach enables:

- Validation of infrastructure changes before AWS deployment
- Consistent development environments across teams
Expand Down Expand Up @@ -167,7 +166,7 @@ This sample application demonstrates how to build, test, and deploy a full-stack
- Using Spring Boot profiles to seamlessly switch between LocalStack and AWS environments.
- Implementing real-time updates using Server-Sent Events and SQS message consumption.
- Leveraging Testcontainers for integration testing against LocalStack infrastructure.
- Utilizing `tflocal` and `awslocal` to streamline local development workflows.
- Utilizing `lstk tf` and `lstk aws` to streamline local development workflows.

## Learn More

Expand Down
13 changes: 10 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven-checkstyle-plugin.version>3.2.0</maven-checkstyle-plugin.version>
<testcontainers.version>1.19.7</testcontainers.version>
<testcontainers.version>2.0.5</testcontainers.version>
<lombok.version>1.18.46</lombok.version>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
Expand Down Expand Up @@ -85,13 +86,13 @@
<!-- LocalStack TestContainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>localstack</artifactId>
<artifactId>testcontainers-localstack</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
Expand Down Expand Up @@ -121,6 +122,12 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- spring-boot-dependencies pins commons-lang3 below what testcontainers' commons-compress needs -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
Expand Down
4 changes: 2 additions & 2 deletions shipment-picture-lambda-validator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<version>1.18.46</version>
<scope>compile</scope>
</dependency>
</dependencies>
Expand Down Expand Up @@ -93,7 +93,7 @@
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<version>1.18.46</version>
</path>
</annotationProcessorPaths>
</configuration>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

#you need awslocal cli installed for this
#you need lstk aws cli installed for this

awslocal lambda update-function-code --function-name shipment-picture-lambda-validator \
lstk aws lambda update-function-code --function-name shipment-picture-lambda-validator \
--zip-file fileb://target/shipment-picture-lambda-validator.jar \
--region us-east-1

aws lambda update-function-code --function-name shipment-picture-lambda-validator \
lstk aws lambda update-function-code --function-name shipment-picture-lambda-validator \
--zip-file fileb://target/shipment-picture-lambda-validator.jar \
--region us-east-1
2 changes: 0 additions & 2 deletions src/main/resources/buckets.properties

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.Container.ExecResult;
import org.testcontainers.containers.ExecConfig;
import org.testcontainers.containers.localstack.LocalStackContainer;
import org.testcontainers.containers.localstack.LocalStackContainer.Service;
import org.testcontainers.containers.output.Slf4jLogConsumer;
Expand Down Expand Up @@ -68,6 +69,17 @@ public class LocalStackSetupConfigurations {
private static final String LOCALSTACK_AUTH_TOKEN = System.getenv().getOrDefault(
"LOCALSTACK_AUTH_TOKEN", "");

static {
if (LOCALSTACK_AUTH_TOKEN.isBlank()) {
// fail fast here: without this, the container starts, LocalStack exits immediately for
// lack of a license, and Testcontainers still waits out its ~60s readiness timeout before
// surfacing that failure, which just looks like a hang.
throw new IllegalStateException(
"LOCALSTACK_AUTH_TOKEN is not set. Find your token at "
+ "https://app.localstack.cloud/workspace/auth-token");
}
}

@Container
protected static LocalStackContainer localStack =
new LocalStackContainer(DockerImageName.parse("localstack/localstack:latest"))
Expand Down Expand Up @@ -239,12 +251,12 @@ protected static void createBucketNotificationConfiguration()
// lambda needs to be in state "Active" in order to proceed with adding permissions
// this can take 2-3 seconds to reach
var result = localStack.execInContainer(formatCommand(
"awslocal lambda get-function --function-name shipment-picture-lambda-validator"));
"aws --endpoint-url=http://localhost:4566 lambda get-function --function-name shipment-picture-lambda-validator"));
var obj = new JSONObject(result.getStdout()).getJSONObject("Configuration");
var state = obj.getString("State");
while (!state.equals("Active")) {
result = localStack.execInContainer(formatCommand(
"awslocal lambda get-function --function-name shipment-picture-lambda-validator"));
"aws --endpoint-url=http://localhost:4566 lambda get-function --function-name shipment-picture-lambda-validator"));
obj = new JSONObject(result.getStdout()).getJSONObject("Configuration");
state = obj.getString("State");
}
Expand Down Expand Up @@ -440,7 +452,17 @@ protected static ExecResult executeInContainer(String command) throws Exception
return execResult;
}

private static String[] formatCommand(String command) {
return command.split(" ");
// lstk can't run here (it's a host-side proxy with no in-container docker/config access);
// the image already bundles the plain aws CLI, so we point it at the gateway directly instead.
private static final Map<String, String> AWS_CLI_ENV = Map.of(
"AWS_ACCESS_KEY_ID", "test",
"AWS_SECRET_ACCESS_KEY", "test",
"AWS_DEFAULT_REGION", "us-east-1");

private static ExecConfig formatCommand(String command) {
return ExecConfig.builder()
.command(command.split(" "))
.envVars(AWS_CLI_ENV)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public String getFilename() {

assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
var execResult = executeInContainer(
"awslocal s3api list-objects --bucket shipment-picture-bucket --query length(Contents[])");
"aws --endpoint-url=http://localhost:4566 s3api list-objects --bucket shipment-picture-bucket --query length(Contents[])");
assertEquals(String.valueOf(1), execResult.getStdout().trim());
}

Expand Down
Loading