Skip to content

Commit aa856a8

Browse files
committed
Create map of service discovery entries as environment variables stored
in S3 for the different task definitions to reference. User code running in the containers can now reference internal services similar to how Kubernetes works by looking for environment variables called SERVICE_<servicename>_HOST and SERVICE_<servicename>_PORT
1 parent b5fa275 commit aa856a8

File tree

7 files changed

+95
-41
lines changed

7 files changed

+95
-41
lines changed

functions/core-stack-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/CoreStackListener.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,21 +84,21 @@ public Object handleRequest(SNSEvent event, Context context) {
8484
ListStackResourcesResponse resources = cfn.listStackResources(req -> req.stackName(stackIdName));
8585
for (StackResourceSummary resource : resources.stackResourceSummaries()) {
8686
String resourceType = resource.resourceType();
87-
String physicalResourceId = resource.physicalResourceId();
87+
String ecrRepo = resource.physicalResourceId();
8888
String resourceStatus = resource.resourceStatusAsString();
89-
String logicalId = resource.logicalResourceId();
90-
LOGGER.info("Processing resource {} {} {} {}", resourceType, resourceStatus, logicalId,
91-
physicalResourceId);
89+
String serviceName = resource.logicalResourceId();
90+
LOGGER.info("Processing resource {} {} {} {}", resourceType, resourceStatus, serviceName,
91+
ecrRepo);
9292
if ("CREATE_COMPLETE".equals(resourceStatus)) {
9393
if ("AWS::ECR::Repository".equals(resourceType)) {
94-
LOGGER.info("Publishing appConfig update event for ECR repository {} {}", logicalId,
95-
physicalResourceId);
94+
LOGGER.info("Publishing appConfig update event for ECR repository {} {}", serviceName,
95+
ecrRepo);
9696
// Logical ID is the service name
9797
// Physical ID is the repo name
9898
Map<String, Object> systemApiRequest = new HashMap<>();
99-
systemApiRequest.put("resource", "settings/config/" + logicalId + "/ECR_REPO");
99+
systemApiRequest.put("resource", "settings/config/" + serviceName + "/ECR_REPO");
100100
systemApiRequest.put("method", "PUT");
101-
systemApiRequest.put("body", Utils.toJson(Map.of("value", physicalResourceId)));
101+
systemApiRequest.put("body", Utils.toJson(Map.of("value", ecrRepo)));
102102
publishEvent(systemApiRequest, SYSTEM_API_CALL);
103103
}
104104
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"records": [
3+
{
4+
"sns": {
5+
"message": "StackId='arn:aws:cloudformation:us-west-2:914245659875:stack/sb-multi-tenant-e2f85934/3b77ee40-5a03-11ec-8817-02418856b67f'\nLogicalResourceId='sb-multi-tenant-e2f85934'\nPhysicalResourceId='arn:aws:cloudformation:us-west-2:914245659875:stack/sb-multi-tenant-e2f85934/3b77ee40-5a03-11ec-8817-02418856b67f'\nResourceStatus='CREATE_COMPLETE'\nResourceType='AWS::CloudFormation::Stack'\nStackName='sb-multi-tenant-e2f85934'"
6+
}
7+
}
8+
]
9+
}

functions/onboarding-listener/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingListener.java

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,6 @@ public Object handleRequest(SNSEvent event, Context context) {
197197
} else {
198198
// Match on the resource type and build the console url
199199
for (AwsResource awsResource : AwsResource.values()) {
200-
String name = null;
201-
String arn = null;
202200
if (awsResource.getResourceType().equalsIgnoreCase(resourceType)) {
203201
if ("AWS::ElasticLoadBalancingV2::LoadBalancer".equals(resourceType)) {
204202
// CloudFormation returns the ARN for the physical id of the load balancer
@@ -399,28 +397,28 @@ protected String getTenantIdFromStackParameters(String stackId) {
399397
return tenantId;
400398
}
401399

402-
private void getRdsResources(String stackId, Map<String, String> consoleResources) {
403-
try {
404-
ListStackResourcesResponse resources = cfn.listStackResources(request -> request.stackName(stackId));
405-
for (StackResourceSummary resource : resources.stackResourceSummaries()) {
406-
String resourceType = resource.resourceType();
407-
String physicalResourceId = resource.physicalResourceId();
408-
AwsResource url = null;
409-
if (resourceType.equalsIgnoreCase(AwsResource.RDS_INSTANCE.getResourceType())) {
410-
url = AwsResource.RDS_INSTANCE;
411-
} else if (resourceType.equalsIgnoreCase(AwsResource.RDS_CLUSTER.getResourceType())) {
412-
url = AwsResource.RDS_CLUSTER;
413-
}
414-
if (url != null) {
415-
consoleResources.put(url.name(), url.formatUrl(AWS_REGION, physicalResourceId));
416-
}
417-
}
418-
} catch (SdkServiceException cfnError) {
419-
LOGGER.error("cfn:ListStackResources error", cfnError);
420-
LOGGER.error(Utils.getFullStackTrace(cfnError));
421-
throw cfnError;
422-
}
423-
}
400+
// private void getRdsResources(String stackId, Map<String, String> consoleResources) {
401+
// try {
402+
// ListStackResourcesResponse resources = cfn.listStackResources(request -> request.stackName(stackId));
403+
// for (StackResourceSummary resource : resources.stackResourceSummaries()) {
404+
// String resourceType = resource.resourceType();
405+
// String physicalResourceId = resource.physicalResourceId();
406+
// AwsResource url = null;
407+
// if (resourceType.equalsIgnoreCase(AwsResource.RDS_INSTANCE.getResourceType())) {
408+
// url = AwsResource.RDS_INSTANCE;
409+
// } else if (resourceType.equalsIgnoreCase(AwsResource.RDS_CLUSTER.getResourceType())) {
410+
// url = AwsResource.RDS_CLUSTER;
411+
// }
412+
// if (url != null) {
413+
// consoleResources.put(url.name(), url.formatUrl(AWS_REGION, physicalResourceId));
414+
// }
415+
// }
416+
// } catch (SdkServiceException cfnError) {
417+
// LOGGER.error("cfn:ListStackResources error", cfnError);
418+
// LOGGER.error(Utils.getFullStackTrace(cfnError));
419+
// throw cfnError;
420+
// }
421+
// }
424422

425423
private void publishEvent(Map<String, Object> eventBridgeDetail, String detailType) {
426424
try {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.amazon.aws.partners.saasfactory.saasboost;
2+
3+
import org.junit.Test;
4+
5+
public class OnboardingListenerTest {
6+
7+
@Test
8+
public void testHandleRequest() {
9+
10+
}
11+
}

functions/workload-deploy/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/WorkloadDeploy.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ public Object handleRequest(Map<String, Object> event, Context context) {
132132
}
133133
for (Map<String, Object> tenant : getTenantsResponse) {
134134
String tenantId = (String) tenant.get("id");
135+
//TODO need to find the pipeline for each application service
135136
String tenantCodePipeline = "tenant-" + tenantId.substring(0, tenantId.indexOf("-"));
136137
Deployment deployment = new Deployment(tenantId, imageUri, tenantCodePipeline);
137138
deployments.add(deployment);

resources/tenant-onboarding-app.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ Resources:
329329
Action:
330330
- s3:GetObject
331331
Resource:
332-
- !Sub 'arn:aws:s3:::{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}/${TenantId}/ServiceDiscovery.env'
332+
- !Sub 'arn:aws:s3:::{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}/tenants/${TenantId}/ServiceDiscovery.env'
333333
- Effect: Allow
334334
Action:
335335
- s3:GetBucketLocation
@@ -457,7 +457,7 @@ Resources:
457457
Value: !Ref MetricsStream
458458
EnvironmentFiles:
459459
- Type: s3
460-
Value: !Sub 'arn:aws:s3:::{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}/${TenantId}/ServiceDiscovery.env'
460+
Value: !Sub 'arn:aws:s3:::{{resolve:ssm:/saas-boost/${Environment}/SAAS_BOOST_BUCKET}}/tenants/${TenantId}/ServiceDiscovery.env'
461461
MountPoints:
462462
!If
463463
- ProvisionEFS

services/onboarding-service/src/main/java/com/amazon/aws/partners/saasfactory/saasboost/OnboardingService.java

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.slf4j.LoggerFactory;
2727
import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider;
2828
import software.amazon.awssdk.core.exception.SdkServiceException;
29+
import software.amazon.awssdk.core.sync.RequestBody;
2930
import software.amazon.awssdk.http.SdkHttpFullRequest;
3031
import software.amazon.awssdk.regions.Region;
3132
import software.amazon.awssdk.services.cloudformation.CloudFormationClient;
@@ -42,9 +43,7 @@
4243
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
4344
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
4445

45-
import java.io.IOException;
46-
import java.io.InputStream;
47-
import java.io.UnsupportedEncodingException;
46+
import java.io.*;
4847
import java.net.URI;
4948
import java.net.URISyntaxException;
5049
import java.net.URLEncoder;
@@ -692,15 +691,13 @@ public APIGatewayProxyResponseEvent provisionApplication(Map<String, Object> eve
692691
List<String> applicationServiceStacks = new ArrayList<>();
693692

694693
Map<String, Integer> pathPriority = getPathPriority(appConfig);
694+
Properties serviceDiscovery = new Properties();
695695

696696
Map<String, Object> services = (Map<String, Object>) appConfig.get("services");
697697
for (Map.Entry<String, Object> serviceConfig : services.entrySet()) {
698698
String serviceName = serviceConfig.getKey();
699699
// CloudFormation resource names can only contain alpha numeric characters or a dash
700700
String serviceResourceName = serviceName.replaceAll("[^0-9A-Za-z-]", "").toLowerCase();
701-
// If there are any private services, we will create an environment variables called
702-
// SERVICE_<SERVICE_NAME>_HOST and SERVICE_<SERVICE_NAME>_PORT
703-
// to pass to the task definitions
704701
Map<String, Object> service = (Map<String, Object>) serviceConfig.getValue();
705702
Boolean isPublic = (Boolean) service.get("public");
706703
String pathPart = (isPublic) ? (String) service.get("path") : "";
@@ -717,6 +714,17 @@ public APIGatewayProxyResponseEvent provisionApplication(Map<String, Object> eve
717714
String containerRepo = (String) service.get("containerRepo");
718715
String imageTag = (String) service.getOrDefault("containerTag", "latest");
719716

717+
// If there are any private services, we will create an environment variables called
718+
// SERVICE_<SERVICE_NAME>_HOST and SERVICE_<SERVICE_NAME>_PORT to pass to the task definitions
719+
String serviceEnvName = serviceName.replaceAll("\\s+", "_").toUpperCase();
720+
String serviceHost = "SERVICE_" + serviceEnvName + "_HOST";
721+
String servicePort = "SERVICE_" + serviceEnvName + "_PORT";
722+
if (!isPublic) {
723+
LOGGER.debug("Creating service discovery environment variables {}, {}", serviceHost, servicePort);
724+
serviceDiscovery.put(serviceHost, serviceResourceName + ".local");
725+
serviceDiscovery.put(servicePort, Objects.toString(containerPort));
726+
}
727+
720728
Map<String, Object> tiers = (Map<String, Object>) service.get("tiers");
721729
if (!tiers.containsKey(tier)) {
722730
dal.updateStatus(onboarding.getId(), OnboardingStatus.failed);
@@ -862,7 +870,8 @@ public APIGatewayProxyResponseEvent provisionApplication(Map<String, Object> eve
862870

863871
// Make the stack name look like what CloudFormation would have done for a nested stack
864872
String tenantShortId = tenantId.toString().substring(0, 8);
865-
String stackName = "sb-" + SAAS_BOOST_ENV + "-tenant-" + tenantShortId + "-app-" + serviceResourceName + "-" + Utils.randomString(12).toUpperCase();
873+
String stackName = "sb-" + SAAS_BOOST_ENV + "-tenant-" + tenantShortId + "-app-" + serviceResourceName
874+
+ "-" + Utils.randomString(12).toUpperCase();
866875
if (stackName.length() > 128) {
867876
stackName = stackName.substring(0, 128);
868877
}
@@ -896,6 +905,32 @@ public APIGatewayProxyResponseEvent provisionApplication(Map<String, Object> eve
896905
}
897906
}
898907

908+
if (!serviceDiscovery.isEmpty()) {
909+
String environmentFile = "tenants/" + tenantId.toString() + "/ServiceDiscovery.env";
910+
ByteArrayOutputStream environmentFileContents = new ByteArrayOutputStream();
911+
try (Writer writer = new BufferedWriter(new OutputStreamWriter(
912+
environmentFileContents, StandardCharsets.UTF_8)
913+
)) {
914+
serviceDiscovery.store(writer, null);
915+
s3.putObject(request -> request
916+
.bucket(SAAS_BOOST_BUCKET)
917+
.key(environmentFile)
918+
.build(),
919+
RequestBody.fromBytes(environmentFileContents.toByteArray())
920+
);
921+
} catch (SdkServiceException s3Error) {
922+
LOGGER.error("Error putting service discovery file to S3");
923+
LOGGER.error(Utils.getFullStackTrace(s3Error));
924+
dal.updateStatus(onboarding.getId(), OnboardingStatus.failed);
925+
throw s3Error;
926+
} catch (IOException ioe) {
927+
LOGGER.error("Error writing service discovery data to output stream");
928+
LOGGER.error(Utils.getFullStackTrace(ioe));
929+
dal.updateStatus(onboarding.getId(), OnboardingStatus.failed);
930+
throw new RuntimeException(ioe);
931+
}
932+
}
933+
899934
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
900935
.withStatusCode(200)
901936
.withHeaders(CORS)

0 commit comments

Comments
 (0)