Managing multiple service endpoints and credentials for external service calls

   Exchange Pre-configured Maestro services.  |   Platform Developer |  All versions   This feature is related to v5.1 and higher.

As we create services in Journey Manager that integrate with external sources via Web Service or REST we typically find that the external service endpoint and credentials change between Journey Manager environments (dev, test, prod). Or alternatively, you may have created a mock service endpoint for use during early development phases and want the ability to easily switch between the mock service and the genuine service for debugging and to verify expected results. This article describes the best practice approach to managing these different endpoint configurations using Service Connections.

Creating Service Connections

The best practice approach to managing multiple endpoint configurations is to utilize Service Connections. Lets look at an example.

Say we have a requirement to build a Dynamic Data service for use by forms that takes a US Zip Code and returns the City, State, Area Code and Time Zone. This service will call out to an external API to retrieve this information. Assume the external service provider has exposed a test service for use in dev/test environments which we will use until we deploy to the production server.

  1. When creating the Service Connection for this mock endpoint I use the generic HTTP Endpoint type, then enter the name and endpoint URI. While this service does not require authentication credentials, I could just as easily add them if they were required.

  2. As we are developing this service we want to access a mock endpoint to control the response format and verify the behavior of my service, and so after building a mock service using on a mock platform (e.g. mockable.io) I can create a new service connection that points to it.

Using Service Connections in Groovy Services

  1. To facilitate the dynamic data calls from the form we need to create a new service of type 'Dynamic Data', using the Groovy Dynamic Data template.

  2. When configuring the service we can select the Service Connection we wish to use. We will start with the ZIP Lookup Mock endpoint.

  3. Now in our Groovy Script we can retrieve the service connection from the service definition object and validate that it is configured correctly.

    // Ensure that a Service Connection is selected def serviceConnection = serviceDefinition.connection if(!serviceConnection){ throw new IllegalArgumentException("No Service Connection defined for this service.") } // Retrieve the base endpoint URL from the service connection. def endpoint = serviceConnection.endpointValue if(!endpoint){ throw new IllegalArgumentException("No endpoint defined for the service connection.") }

  4. Having a valid endpoint to work with we can now setup the remote request, apply basic authentication credentials if present, execute the call before marshaling the response.

    // Create the remote request def remoteRequest = new GetRequest(endpoint) // If auth details provided, set them in the remote request if(serviceConnection.username){ remoteRequest.setBasicAuth(serviceConnection.username, serviceConnection.password) }   // Now execute the remote call remoteRequest.setParams(["USZip": zipInput]) def response = remoteRequest.execute()  

  5. At any time we can switch between the mock service and the genuine service by selecting the appropriate Service Connection in the Service Definition.

    >Full Groovy Script /* Provides form lookup form data service by calling a configurable Groovy script.   Script parameters include: form : com.avoka.fc.core.entity.Form request : javax.servlet.http.HttpServletRequest submission : com.avoka.fc.core.entity.Submission serviceDefinition : com.avoka.fc.core.entity.ServiceDefinition serviceParameters : Map<String, String>   Script return: JSON string data value to be bound into the form data model import com.avoka.component.http.GetRequest import groovy.json.JsonBuilder import groovy.util.XmlSlurper   def zipInput = "" + request.getParameter('zip') logger.info "Zip: " + zipInput >if(!zipInput){ throw new IllegalArgumentException("Missing input parameter - zip") }   // Ensure that a Service Connection is selected def serviceConnection = serviceDefinition.connection if(!serviceConnection){ throw new IllegalArgumentException("No Service Connection defined for this service.") }   // Retrieve the base endpoint URL from the service connection. def endpoint = serviceConnection.endpointValue if(!endpoint){ throw new IllegalArgumentException("No endpoint defined for the service connection.") }   // Create the remote request def remoteRequest = new GetRequest(endpoint) // If auth details provided, set them in the remote request if(serviceConnection.username){ remoteRequest.setBasicAuth(serviceConnection.username, serviceConnection.password) }   // Now execute the remote call remoteRequest.setParams(["USZip": zipInput]) def response = remoteRequest.execute()   // check HttpResponse status code if (response.status == 404) { // object not found throw new RuntimeException('Could not find the requested service: ' + response.statusLine) } else if (response.status != 200) { throw new RuntimeException(response.statusLine) }   logger.info response.textContent   // Service returns XML so lets parse it for a JSON response def parsed = new XmlSlurper().parseText(response.textContent) def childNodes = parsed.Table.children().iterator().toList() def responseValues = childNodes.collectEntries( [:] ) { [(it.name().toLowerCase()): (it.text())] }   logger.info responseValues   // Build the JSON response def serviceResponse = new JsonBuilder(responseValues).toPrettyString() logger.info "Response: " + serviceResponse   return serviceResponse  

Deployment Practices

So what happens when we want to push this service into another environment with potentially different endpoint configurations? The following practices will help make this a very seamless process.

  1. Ensure the genuine service connection is selected before export.

    When exporting a service from one environment to another, if a Service Connection is selected in the Service Definition it will also be exported into the archive file and available when you go to import the archive to another environment. So if you are using multiple endpoints in the development environment, ensure that you have the appropriate service connection selected when you go to export, i.e. the one you want to be deployed into the archive. It is unlikely that you will want to deploy the mock service to other environments, so usually you will want to select the genuine service.

  2. Import with 'Preserve Existing Service Connections' selected.

    When importing a service archive into the target environment you are offered several import options. Check the help against each of these options and be aware how they impact the import behavior. Of most relevance to this article is the 'Preserve Existing Service Connections' option. If the import archive contains a Service Connection with the same name (and version) as an existing Service Connection in the target environment, it will not get imported if this option is selected. If no Service Connection exists with the same name then it will always be created. Typically, you will want this option enabled (which it is by default) so that changes to the endpoint configurations do not get overwritten each time you import.

  3. Use consistent naming of Service Connections between environments.

    Ensuring that you use consistent naming of Service Connections between environments will avoid any manual reconfiguration each time you import a service from one environment to another. It is entirely up to you whether you per-configure the Service Connections in each environment or wait until the Service Connection is imported into each environment and modify the configurations at that point. In production environments you can configure the endpoint to point to the live endpoint with production credentials (if required) and if using the practices above, you can have confidence that these endpoint configurations will be maintained between import events.

Calling Multiple Endpoints in a Single Service

The practices detailed above assume that your groovy service is only required to call a single remote service, but what if it is required to call several remote services in a single execution?

  1. To handle this type of scenario you will need to create a service parameter for each of the remote services, that stores the name of the Service Connection to use.

  2. Within your groovy script you can retrieve each service connection by name using the ServiceConnectionDao class and then execute your remote call as normal.

    // Check the endpoint parameter is defined def zipConnectionName = serviceParameters.get('GetInfoByZip Connection Name') if(!zipConnectionName){ throw new IllegalArgumentException("Missing parameter value for: GetInfoByZip Connection Name")} // Ensure that a Service Connection is selected def serviceConnection = new com.avoka.fc.core.dao.ServiceConnectionDao().getServiceConnectionForName(zipConnectionName) if(!serviceConnection){ throw new IllegalArgumentException("No Service Connection defined for this service.") }

    Note that where Service Connections are referenced by parameter values in this manner they will not be exported into the service archive and must be manually created in other environments.