Repository: LangStream/langstream (language: python)

Issue:
langstream deployment issues on Azure AKS
I am installing langstream in Azure Kubernetes Service environment, I have followed the steps for the deployment in the kubernetes.. I have used helm charts for the install.. I used the below commands for the deployment..

helm repo add langstream https://langstream.github.io/charts
helm repo update
helm install -n langstream --create-namespace langstream langstream/langstream --values helm/examples/simple.yaml

naga_bondada [ ~/langstream ]$ kubectl get pods -n langstream
NAME                                        READY   STATUS             RESTARTS         AGE
langstream-api-gateway-79bd776d6-2npsd      1/1     Running            0                31m
langstream-client-d5b455cc6-gwd4d           1/1     Running            0                31m
langstream-control-plane-7b7fb7ffc5-bj85h   0/1     CrashLoopBackOff   10 (4m15s ago)   31m
langstream-deployer-57b6565484-zsjxx        1/1     Running            0                31m
naga_bondada [ ~/langstream ]$ 

I am unable to find runtime pod in the list of pods.. below is the output from the control plane pods logs..

[controlplane-pod-logs.txt](https://github.com/LangStream/langstream/files/12596466/controlplane-pod-logs.txt)

Please help me in resolving this issue to proceed further..

Suresh


Discussion / hints:
@sreyabondada 👋 
>I am unable to find runtime pod in the list of pods

This is fine. The runtime pod is scheduled only if you create an application

> --values helm/examples/simple.yaml

These values are deprecated and not mantained. Can you install it using the default values ? 

```
helm install -n langstream --create-namespace langstream langstream/langstream
``` 

or if you want to keep the existing one

```
helm upgrade -n langstream langstream langstream/langstream
```
Hi Nicolo
Thanks for your reply, its working now.. which document we need to use going further..  Looks like the current document is not upto date..
Please suggest.

Regards
Suresh
@sreyabondada I updated the readme and that values file.

First of all you need to configure the code storage, you can find instructions [here](https://github.com/LangStream/langstream/tree/main/helm)
You either setup a local MinIO instance or use a real S3 bucket.

Once you're there, you are able to deploy applications.

First, install the CLI: https://docs.langstream.ai/installation/langstream-cli
Then create your application. You can take a look at the [examples](https://github.com/LangStream/langstream/tree/main/examples/applications) directory. 

Additionally, here is the official [documentation](https://docs.langstream.ai/get-started#your-first-application) 

Hi Nicolo

We have one more query, We are setting up langstream on Azure and we got to know Azure Blob Storage doesn't support S3 protocol.. how we can make use of langstream in that scenerio.. Could you please throw some light here.

Regards
Suresh

Fix patch:
diff --git a/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/pom.xml b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/pom.xml
new file mode 100644
index 000000000..f2dff2a3a
--- /dev/null
+++ b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/pom.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright DataStax, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <artifactId>langstream-codestorage-providers</artifactId>
+    <groupId>ai.langstream</groupId>
+    <version>0.0.17-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>langstream-codestorage-azure-blob-storage</artifactId>
+
+  <properties>
+    <azure-sdk-bom.version>1.2.6</azure-sdk-bom.version>
+  </properties>
+
+
+  <dependencyManagement>
+    <dependencies>
+      <dependency>
+        <groupId>com.azure</groupId>
+        <artifactId>azure-sdk-bom</artifactId>
+        <version>${azure-sdk-bom.version}</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+    </dependencies>
+  </dependencyManagement>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>langstream-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+<!--      <scope>provided</scope>-->
+    </dependency>
+
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-core</artifactId>
+      <!--      <scope>provided</scope>-->
+    </dependency>
+    <dependency>
+      <groupId>com.azure</groupId>
+      <artifactId>azure-storage-blob</artifactId>
+      <exclusions>
+        <exclusion>
+          <groupId>com.azure</groupId>
+          <artifactId>azure-core-http-netty</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>com.azure</groupId>
+      <artifactId>azure-core-http-okhttp</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorage.java b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorage.java
new file mode 100644
index 000000000..ce7725a97
--- /dev/null
+++ b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorage.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ai.langstream.impl.storage.k8s.codestorage;
+
+import ai.langstream.api.codestorage.CodeArchiveMetadata;
+import ai.langstream.api.codestorage.CodeStorage;
+import ai.langstream.api.codestorage.CodeStorageException;
+import ai.langstream.api.codestorage.LocalZipFileArchiveFile;
+import ai.langstream.api.codestorage.UploadableCodeArchive;
+import com.azure.core.http.rest.Response;
+import com.azure.core.util.BinaryData;
+import com.azure.core.util.Context;
+import com.azure.storage.blob.BlobClient;
+import com.azure.storage.blob.BlobContainerClient;
+import com.azure.storage.blob.BlobContainerClientBuilder;
+import com.azure.storage.blob.models.BlobHttpHeaders;
+import com.azure.storage.blob.models.BlockBlobItem;
+import com.azure.storage.blob.options.BlobParallelUploadOptions;
+import com.azure.storage.common.StorageSharedKeyCredential;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.UUID;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.NotNull;
+
+@Slf4j
+public class AzureBlobCodeStorage implements CodeStorage {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+    // metadata keys must not contain dashes since it's not supported by azure
+    protected static final String OBJECT_METADATA_KEY_TENANT = "langstreamtenant";
+    protected static final String OBJECT_METADATA_KEY_APPLICATION = "langstreamapplication";
+    protected static final String OBJECT_METADATA_KEY_VERSION = "langstreamversion";
+    protected static final String OBJECT_METADATA_KEY_PY_BINARIES_DIGEST =
+            "langstreampybinariesdigest";
+    protected static final String OBJECT_METADATA_KEY_JAVA_BINARIES_DIGEST =
+            "langstreamjavabinariesdigest";
+    private final BlobContainerClient containerClient;
+
+    @SneakyThrows
+    public AzureBlobCodeStorage(Map<String, Object> configuration) {
+        final AzureBlobCodeStorageConfiguration azureConfig =
+                mapper.convertValue(configuration, AzureBlobCodeStorageConfiguration.class);
+
+        if (azureConfig.getEndpoint() == null) {
+            throw new IllegalArgumentException("Azure 'endpoint' must be provided");
+        }
+
+        BlobContainerClientBuilder containerClientBuilder = new BlobContainerClientBuilder();
+        if (azureConfig.getSasToken() != null) {
+            containerClientBuilder.sasToken(azureConfig.getSasToken());
+            log.info("Connecting to Azure at {} with SAS token", azureConfig.getEndpoint());
+        } else if (azureConfig.getStorageAccountName() != null) {
+            containerClientBuilder.credential(
+                    new StorageSharedKeyCredential(
+                            azureConfig.getStorageAccountName(),
+                            azureConfig.getStorageAccountKey()));
+            log.info(
+                    "Connecting to Azure at {} with account name {}",
+                    azureConfig.getEndpoint(),
+                    azureConfig.getStorageAccountName());
+        } else if (azureConfig.getStorageAccountConnectionString() != null) {
+            log.info("Connecting to Azure at {} with connection string", azureConfig.getEndpoint());
+            containerClientBuilder.credential(
+                    StorageSharedKeyCredential.fromConnectionString(
+                            azureConfig.getStorageAccountConnectionString()));
+        } else {
+            throw new IllegalArgumentException(
+                    "Either sas-token, account-name/account-key or account-connection-string must be provided");
+        }
+
+        containerClientBuilder.endpoint(azureConfig.getEndpoint());
+        final String container = azureConfig.getContainer();
+        containerClientBuilder.containerName(container);
+        this.containerClient = containerClientBuilder.buildClient();
+        log.info(
+                "Connected to Azure to account {}, container {}",
+                containerClient.getAccountName(),
+                containerClient.getBlobContainerName());
+
+        if (!this.containerClient.exists()) {
+            log.info("Creating container");
+            this.containerClient.createIfNotExists();
+        } else {
+            log.info("Container already exists");
+        }
+    }
+
+    @Override
+    public CodeArchiveMetadata storeApplicationCode(
+            String tenant, String applicationId, String version, UploadableCodeArchive codeArchive)
+            throws CodeStorageException {
+
+        try {
+            Path tempFile = Files.createTempFile("langstream", "upload");
+            try {
+                Files.copy(codeArchive.getData(), tempFile, StandardCopyOption.REPLACE_EXISTING);
+                String codeStoreId =
+                        tenant + "_" + applicationId + "_" + version + "_" + UUID.randomUUID();
+                log.info(
+                        "Storing code archive {} for tenant {} and application {} with version {}",
+                        codeStoreId,
+                        tenant,
+                        applicationId,
+                        version);
+
+                final String javaBinariesDigest = codeArchive.getJavaBinariesDigest();
+                final String pyBinariesDigest = codeArchive.getPyBinariesDigest();
+
+                final Map<String, String> userMetadata = new HashMap<>();
+                userMetadata.put(OBJECT_METADATA_KEY_TENANT, tenant);
+                userMetadata.put(OBJECT_METADATA_KEY_APPLICATION, applicationId);
+                userMetadata.put(OBJECT_METADATA_KEY_VERSION, version);
+                if (javaBinariesDigest != null) {
+                    userMetadata.put(OBJECT_METADATA_KEY_JAVA_BINARIES_DIGEST, javaBinariesDigest);
+                }
+                if (pyBinariesDigest != null) {
+                    userMetadata.put(OBJECT_METADATA_KEY_PY_BINARIES_DIGEST, pyBinariesDigest);
+                }
+
+                final BlobParallelUploadOptions options =
+                        new BlobParallelUploadOptions(BinaryData.fromFile(tempFile))
+                                .setMetadata(userMetadata)
+                                .setHeaders(
+                                        new BlobHttpHeaders().setContentType("application/zip"));
+
+                final BlobClient blobClient = getBlobClient(tenant, codeStoreId);
+                final Response<BlockBlobItem> response =
+                        blobClient.uploadWithResponse(options, null, Context.NONE);
+                if (response.getValue() == null) {
+                    throw new CodeStorageException(
+                            "Failed to upload code archive to azure, status code: "
+                                    + response.getStatusCode()
+                                    + ". value was null.");
+                }
+
+                return new CodeArchiveMetadata(
+                        tenant, codeStoreId, applicationId, pyBinariesDigest, javaBinariesDigest);
+            } finally {
+                Files.delete(tempFile);
+            }
+        } catch (IOException err) {
+            throw new CodeStorageException(err);
+        }
+    }
+
+    @Override
+    public void downloadApplicationCode(
+            String tenant, String codeStoreId, DownloadedCodeHandled codeArchive)
+            throws CodeStorageException {
+        try {
+            Path tempFile = Files.createTempDirectory("langstream-download-code");
+            Path zipFile = tempFile.resolve("code.zip");
+            try {
+
+                getBlobClient(tenant, codeStoreId)
+                        .getBlockBlobClient()
+                        .downloadToFile(zipFile.toString());
+                codeArchive.accept(new LocalZipFileArchiveFile(zipFile));
+            } finally {
+                if (Files.exists(zipFile)) {
+                    Files.delete(zipFile);
+                }
+                Files.delete(tempFile);
+            }
+        } catch (IOException e) {
+            log.error("Error downloading code archive {} for tenant {}", codeStoreId, tenant, e);
+            throw new CodeStorageException(e);
+        }
+    }
+
+    @Override
+    public CodeArchiveMetadata describeApplicationCode(String tenant, String codeStoreId)
+            throws CodeStorageException {
+        final BlobClient blobClient = getBlobClient(tenant, codeStoreId);
+        final Boolean exists = blobClient.exists();
+        if (exists != null && exists) {
+            final Map<String, String> metadata =
+                    blobClient.getBlockBlobClient().getProperties().getMetadata();
+            final String objectTenant = metadata.get(OBJECT_METADATA_KEY_TENANT);
+            if (!Objects.equals(objectTenant, tenant)) {
+                throw new CodeStorageException(
+                        "Tenant mismatch in Azure object "
+                                + blobClient.getBlobName()
+                                + ": "
+                                + objectTenant
+                                + " != "
+                                + tenant);
+            }
+            final String applicationId = metadata.get(OBJECT_METADATA_KEY_APPLICATION);
+            Objects.requireNonNull(
+                    applicationId,
+                    "Blob " + blobClient.getBlobName() + " contains empty application metadata");
+
+            final String pyBinariesDigest = metadata.get(OBJECT_METADATA_KEY_PY_BINARIES_DIGEST);
+            final String javaBinariesDigest =
+                    metadata.get(OBJECT_METADATA_KEY_JAVA_BINARIES_DIGEST);
+
+            return new CodeArchiveMetadata(
+                    tenant, codeStoreId, applicationId, pyBinariesDigest, javaBinariesDigest);
+        } else {
+            return null;
+        }
+    }
+
+    @NotNull
+    private BlobClient getBlobClient(String tenant, String codeStoreId) {
+        final String key = tenant + "-" + codeStoreId;
+        final BlobClient blobClient = containerClient.getBlobClient(key);
+        return blobClient;
+    }
+
+    @Override
+    public void deleteApplicationCode(String tenant, String codeStoreId)
+            throws CodeStorageException {
+        getBlobClient(tenant, codeStoreId).deleteIfExists();
+    }
+
+    @Override
+    public void deleteApplication(String tenant, String application) throws CodeStorageException {
+        // TODO
+    }
+
+    @Override
+    public void close() {}
+}
diff --git a/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorageConfiguration.java b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorageConfiguration.java
new file mode 100644
index 000000000..4b047bf51
--- /dev/null
+++ b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorageConfiguration.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ai.langstream.impl.storage.k8s.codestorage;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AzureBlobCodeStorageConfiguration {
+    private String type;
+
+    private String container = "langstream-code-storage";
+
+    private String endpoint;
+
+    @JsonAlias({"sastoken"})
+    @JsonProperty("sas-token")
+    private String sasToken;
+
+    @JsonAlias({"storageaccountname"})
+    @JsonProperty("storage-account-name")
+    private String storageAccountName;
+
+    @JsonAlias({"storageaccountkey"})
+    @JsonProperty("storage-account-key")
+    private String storageAccountKey;
+
+    @JsonAlias({"storageaccountconnectionstring"})
+    @JsonProperty("storage-account-connection-string")
+    private String storageAccountConnectionString;
+}
diff --git a/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorageProvider.java b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorageProvider.java
new file mode 100644
index 000000000..1d56ae98d
--- /dev/null
+++ b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/java/ai/langstream/impl/storage/k8s/codestorage/AzureBlobCodeStorageProvider.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright DataStax, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package ai.langstream.impl.storage.k8s.codestorage;
+
+import ai.langstream.api.codestorage.CodeStorage;
+import ai.langstream.api.codestorage.CodeStorageProvider;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class AzureBlobCodeStorageProvider implements CodeStorageProvider {
+
+    @Override
+    public CodeStorage createImplementation(
+            String codeStorageType, Map<String, Object> configuration) {
+        return new AzureBlobCodeStorage(configuration);
+    }
+
+    @Override
+    public boolean supports(String codeStorageType) {
+        return "azure".equals(codeStorageType);
+    }
+}
diff --git a/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/resources/META-INF/services/ai.langstream.api.codestorage.CodeStorageProvider b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/resources/META-INF/services/ai.langstream.api.codestorage.CodeStorageProvider
new file mode 100644
index 000000000..c6b3bce4c
--- /dev/null
+++ b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/main/resources/META-INF/services/ai.langstream.api.codestorage.CodeStorageProvider
@@ -0,0 +1,1 @@
+ai.langstream.impl.storage.k8s.codestorage.AzureBlobCodeStorageProvider
\ No newline at end of file
diff --git a/langstream-codestorage-s3/pom.xml b/langstream-codestorage-providers/langstream-codestorage-s3/pom.xml
similarity index 90%
rename from langstream-codestorage-s3/pom.xml
rename to langstream-codestorage-providers/langstream-codestorage-s3/pom.xml
index b5855b3cc..605f7cbb7 100644
--- a/langstream-codestorage-s3/pom.xml
+++ b/langstream-codestorage-providers/langstream-codestorage-s3/pom.xml
@@ -18,7 +18,7 @@
 -->
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <parent>
-    <artifactId>langstream-ai</artifactId>
+    <artifactId>langstream-codestorage-providers</artifactId>
     <groupId>ai.langstream</groupId>
     <version>0.0.17-SNAPSHOT</version>
   </parent>
@@ -26,11 +26,6 @@
 
   <artifactId>langstream-codestorage-s3</artifactId>
 
-  <properties>
-    <maven.compiler.source>17</maven.compiler.source>
-    <maven.compiler.target>17</maven.compiler.target>
-
-  </properties>
   <dependencies>
     <dependency>
       <groupId>${project.groupId}</groupId>
diff --git a/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorage.java b/langstream-codestorage-providers/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorage.java
similarity index 100%
rename from langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorage.java
rename to langstream-codestorage-providers/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorage.java
diff --git a/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorageConfiguration.java b/langstream-codestorage-providers/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorageConfiguration.java
similarity index 100%
rename from langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorageConfiguration.java
rename to langstream-codestorage-providers/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorageConfiguration.java
diff --git a/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorageProvider.java b/langstream-codestorage-providers/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorageProvider.java
similarity index 100%
rename from langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorageProvider.java
rename to langstream-codestorage-providers/langstream-codestorage-s3/src/main/java/ai/langstream/impl/storage/k8s/codestorage/S3CodeStorageProvider.java
diff --git a/langstream-codestorage-s3/src/main/resources/META-INF/services/ai.langstream.api.codestorage.CodeStorageProvider b/langstream-codestorage-providers/langstream-codestorage-s3/src/main/resources/META-INF/services/ai.langstream.api.codestorage.CodeStorageProvider
similarity index 100%
rename from langstream-codestorage-s3/src/main/resources/META-INF/services/ai.langstream.api.codestorage.CodeStorageProvider
rename to langstream-codestorage-providers/langstream-codestorage-s3/src/main/resources/META-INF/services/ai.langstream.api.codestorage.CodeStorageProvider
diff --git a/langstream-codestorage-providers/pom.xml b/langstream-codestorage-providers/pom.xml
new file mode 100644
index 000000000..d6b55a1bf
--- /dev/null
+++ b/langstream-codestorage-providers/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright DataStax, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>langstream-ai</artifactId>
+        <groupId>ai.langstream</groupId>
+        <version>0.0.17-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>langstream-codestorage-providers</artifactId>
+    <name>LangStream - Code Storage Providers</name>
+    <packaging>pom</packaging>
+    <modules>
+        <module>langstream-codestorage-s3</module>
+        <module>langstream-codestorage-azure-blob-storage</module>
+    </modules>
+</project>
diff --git a/langstream-webservice/pom.xml b/langstream-webservice/pom.xml
index 05882845e..60f3add52 100644
--- a/langstream-webservice/pom.xml
+++ b/langstream-webservice/pom.xml
@@ -73,6 +73,12 @@
 			<version>${project.version}</version>
 			<scope>runtime</scope>
 		</dependency>
+		<dependency>
+			<groupId>${project.groupId}</groupId>
+			<artifactId>langstream-codestorage-azure-blob-storage</artifactId>
+			<version>${project.version}</version>
+			<scope>runtime</scope>
+		</dependency>
 		<dependency>
 			<groupId>${project.groupId}</groupId>
 			<artifactId>langstream-k8s-runtime-core</artifactId>
diff --git a/pom.xml b/pom.xml
index 4c80aa5da..db4fad738 100644
--- a/pom.xml
+++ b/pom.xml
@@ -773,7 +773,7 @@
         <module>langstream-kafka</module>
         <module>langstream-kafka-runtime</module>
         <module>langstream-k8s-runtime</module>
-        <module>langstream-codestorage-s3</module>
+        <module>langstream-codestorage-providers</module>
         <module>langstream-runtime</module>
         <module>langstream-webservice</module>
         <module>langstream-k8s-storage</module>
@@ -801,7 +801,7 @@
         <module>langstream-kafka</module>
         <module>langstream-kafka-runtime</module>
         <module>langstream-k8s-runtime</module>
-        <module>langstream-codestorage-s3</module>
+        <module>langstream-codestorage-providers</module>
         <module>langstream-k8s-storage</module>
         <module>langstream-k8s-common</module>
         <module>langstream-api-gateway-auth</module>


Test patch:
diff --git a/langstream-codestorage-s3/src/test/resources/logback-test.xml b/langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/test/resources/logback-test.xml
similarity index 100%
rename from langstream-codestorage-s3/src/test/resources/logback-test.xml
rename to langstream-codestorage-providers/langstream-codestorage-azure-blob-storage/src/test/resources/logback-test.xml
diff --git a/langstream-codestorage-providers/langstream-codestorage-s3/src/test/resources/logback-test.xml b/langstream-codestorage-providers/langstream-codestorage-s3/src/test/resources/logback-test.xml
new file mode 100644
index 000000000..fdfe741f8
--- /dev/null
+++ b/langstream-codestorage-providers/langstream-codestorage-s3/src/test/resources/logback-test.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+    Copyright DataStax, Inc.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+
+<!DOCTYPE configuration>
+<configuration>
+    <import class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"/>
+    <import class="ch.qos.logback.core.ConsoleAppender"/>
+
+    <appender name="STDOUT" class="ConsoleAppender">
+        <encoder class="PatternLayoutEncoder">
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%kvp- %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <root level="info">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>