Package software.amazon.cloudformation.proxy
This package provide facilities to make it easy to work against AWS APIs that
are eventually consistent for applying resource state. Developers need to
sequence APIs calls in order to effectively apply state. When dependent
resources are not available, e.g. when associating KMS Key with CloudWatch
Log group, the key might not have propogated to region leading to failures to
associate with the log group. This framework provides developers the
facilities to deal with all of these situations using a simple functional
model.
The framework handles the following for developers:
- Taking care of communicating all progress events consistently with CloudFormation including appropriate handshakes
- Handles all client side error reporting correctly with appropriate failure codes and errors messages
- Handles common AWS service exceptions like Throttle and several other retry errors based on HTTP codes
- Can auto-renew STS token expiry transparently if needed or schedule callback from CloudFormation requesting new credentials
- Automatically handles AWS Lambda runtime constraints and schedules callbacks when we near max computation time. Auto adjusts behavior based on the runtime the handler is bound other modes for operating
- Memoize all service call requests, responses, stabilization and retry handling that ensures continuations of handler compute start from where we left off for callbacks.
- Provides default flat retry model for calls of 5 seconds with max timeout of 2 minutes for all API calls. Allows for sensible defaults to be specified for retry strategy by integrator on a per API call basis. Allows framework to dynamically override backoff behavior for helping with emergencies with downstream service latency and other situations defined by customer need
- Provides developers with a simple sequential pattern of progress chaining while doing all the heavy lifting above. Provides most common sensible defaults with override capabilities for integrator to influence as needed. Covers power users and simple users alike.
- Brings consistent model across all service integrations across AWS services on GitHub hosted code base.
Anatomy of an AWS Web Service call
Most AWS web service API calls follows a typical pattern shown below:
- Initiate the call context context with an unique name for making an API
call. This name is used inside the callback context to track the different
parts of the API call sequence being made. All bookkeeping inside the
StdCallbackContextis prefixed with this unique name for tracking E.g.initiator.initiate("logs:CreateLogGroup")uses the AWS IAM action name (recommended naming convention to understand permission in code) when invoking CloudWatchLogs CreateLogGroup API. Developers can retrieve different parts of this API call fromStdCallbackContext.callGraphs()map. They can retrieve theCreatLogGroupRequestusing "logs.CreateLogGroup.request" key and the corresponding responseCreateLogGroupResponseusing "logs.CreateLogGroup.response" key after a successful API call is completed. Developers can use them as needed to get data like ARNs from responses when needed. - Translate the incoming CFN resource model properties to the underlying
service API request. E.g.
translate(translator::translatetocreaterequest)translates incoming CFN resource model toCreateLogGroupRequestfor making the API call. - Make the actual service call
(r, c) -> c.injectCredentialsAndInvokeV2(r, c.client()::createLogGroup))The above code ensures that the right credentials are used to make the service call. Developers can create the AWS services client statically and re-use across all calls without worry. Developers can use response object from the API call to update CloudFormation resource model.Typically we setup ARN for the resource post creation. in Create Handlers. It is essential to set these to communicate back to CloudFormation to indicate the handler did successfully start creation and is in flight. Developers do not need to worry about this handshake, the framework already takes care of this. Developers do need to set primary identifier like ARNs if needed from response object to update incoming resource model. This is shown in bold in code belowreturn initiator.initiate("networkmanager:CreateGlobalNetwork") // // Make the request object to create from the model // .translate(model -> CreateGlobalNetworkRequest.builder() .description(model.getDescription()) .tags(Utils.cfnTagsToSdkTags(model.getTags())) .build()) // // Make the call the create the global network. Delegate to framework to retry all errors and // report all errors as appropriate including service quote exceeded and others. // .call((r, c) -> { CreateGlobalNetworkResponse res = c.injectCredentialsAndInvokeV2(r, c.client()::createGlobalNetwork); GlobalNetwork network = res.globalNetwork(); initiator.getResourceModel().setArn(network.globalNetworkArn()); initiator.getResourceModel().setId(network.globalNetworkId());; return res; }) // // Check to see if Global Network is available to use directly from the response or // stabilize as needed. Update model with Arn and Id as primary identifier on the model // object to communicate to CloudFormation about progress // .stabilize((_request, _response, _client, _model, _context) -> { GlobalNetworkState state = _response.globalNetwork().state(); return state == GlobalNetworkState.AVAILABLE || Utils.globalNetwork(_client, state.globalNetworkId()).state() == GlobalNetworkState.AVAILABLE; }).progress(); - Handle stabilization for the current API. Several AWS services return
immediately from the API call, but the resource isn't ready to be consumed,
e.g. KMS Key, Kinesis steam, others. Developers might need to wait for the
resource to be in a good state, e.g. Kinesis Stream is active before
subsequent updates can be applied to the stream. A stabilization Lambda can
be optionally added that is a predicate function that returns true when the
resource is in the desired state. All Delete Handlers that need to wait for
an AWS resource to be deleted completely, will use this pattern show, as
shown below
initiator.initiate("networkmanager:DeleteGlobalNetwork") // // convert from ResourceModel to DeleteGlobalNetworkRequest // .translate(m -> DeleteGlobalNetworkRequest.builder() .globalNetworkId(m.getId()) .build()) // // Make the call to delete the network // .call((r, c) -> { try { return c.injectCredentialsAndInvokeV2(r, c.client()::deleteGlobalNetwork); } catch (ResourceNotFoundException e) { // Informs CloudFormation that the resources was deleted already throw new software.amazon.cloudformation.exceptions.ResourceNotFoundException(e); } }) // // Wait for global network to transition to complete delete state, which is returned by a // ResourceNotFoundException from describe call below. // .stabilize( (_request, _response, _client, _model, _context) -> { // // if we successfully describe it it still exists!!! // try { globalNetwork(_client, _model.getId()); } catch (ResourceNotFoundException e) { return true; } return false; } ) .done(ignored -> ProgressEvent.success(null, context)); - Optionally handle errors, the framework already handles most errors and retries ones that can retried and communicates error codes when appropriate. This is usually the universal catch all exceptions block that can be used to filter exceptions or handle errors across translate, call and stabilize methods
- Proceed with progressing to the chain next sequence of API calls or
indicate successful completion.
OperationStatus.IN_PROGRESSindicates that we can the proceed to next part of API calls to make for resource configuration. E.g. for CloudWatchLogs LogGroup we first create the LogGroup, then we update retention policy, associated KMS key and finally delegate to Read Handler to return the complete state for the resource
Usually the final step in the sequence returnsreturn createLogGroup(initiator) .then(event -> updateRetentionInDays(initiator, event)) .then(event -> associateKMSKey(initiator, event)) // delegate finally to ReadHandler to return complete resource state .then(event -> new ReadHandler().handleRequest(proxy, request, event.getCallbackContext(), logger));OperationStatus.SUCCESS. If any of the steps in between has an error the chain will be skipped to return the error withOperationStatus.FAILEDstatus and an appropriate error message software.amazon.cloudformation.proxy.ProgressEvent#getMessage() E.g. if associateKMSKey had an error to associate KMS key for CloudWatchLogs to use, the chain would exit with FAILED stauts and appropriate exception message. BothOperationStatus.SUCCESSandOperationStatus.FAILEDare pivot points in the chain that will skip the remainder of the chain.
- Create and Update handlers can share common methods between them to apply update to configuration. Using a common Util class to capture these methods improve sharing and consistency
- Create and Update handler should delegate to Read handler to return the complete state of the resource including readonly properties from the service
- Delete Handler must handle resource not found errors/status always when deleting. This ensures that if the resource was removed out of band of CFN, we also remove it from CFN for correctness. The same handling works for stabilization needs to ensure resource was completed
When to re-use rebinding functionality for the model
Rebinding the model is used when the model is immutable by design and we need to create a new instance of the model for each part in the chain. This is to pure for functional programming constructs. Below is an example for traversing list APIs to iterate over to find object of interest. For each iteration the new model must be rebound.
void discoverIfAlreadyExistsWithAlias() {
ListAliasesResponse aliases = ListAliasesResponse.builder().build();
final BiFunction<CallChain.Initiator<KmsClient, ListAliasesResponse, StdCallbackContext>,
Integer,
ProgressEvent<ListAliasesResponse, StdCallbackContext>> invoker =
(initiator_, iteration) ->
initiator_
.initiate("kms:ListAliases-" + iteration)
.translate(m -> ListAliasesRequest.builder().marker(m.nextMarker()).build())
.call((r, c) -> c.injectCredentialsAndInvokeV2(r, c.client()::listAliases))
.success();
int iterationCount = 0;
do {
CallChain.Initiator<KmsClient, ListAliasesResponse, StdCallbackContext> initiator =
this.initiator.rebindModel(aliases);
ProgressEvent<ListAliasesResponse, StdCallbackContext> result = invoker.apply(initiator, iterationCount);
if (!result.isSuccess()) {
throw new RuntimeException("Error retrieving key aliases " + result.getMessage());
}
aliases = result.getResourceModel();
AliasListEntry entry = aliases.aliases().stream().filter(e -> e.aliasName().equals(KEY_ALIAS)).findFirst()
.orElse(null);
if (entry != null) {
kmsKeyId = entry.targetKeyId();
aliasArn = entry.aliasArn();
break;
}
if (aliases.nextMarker() == null) {
break;
}
++iterationCount;
} while (kmsKeyId == null);
}
In the above code
- The model ListAliasRequest is Immutable object. For the first iteration the model has no next marker
- Model is rebound in the code
this.initiator.rebindModel(aliases)to the latest batch of alias - aliases is reassigned
aliases = result.getResourceModel()for newly retrieved model to rebind for next loop
-
Interface Summary Interface Description CallbackAdapter<T> Interface used to abstract the function of reporting back provisioning progress to the handler callerCallChain This can be used by Read, Create, Update and Delete handlers when invoking AWS services.CallChain.Callback<RequestT,ResponseT,ClientT,ModelT,CallbackT extends StdCallbackContext,ReturnT> All service calls made will use the same call back interface for handling both exceptions as well as actual response received from the call.CallChain.Caller<RequestT,ClientT,ModelT,CallbackT extends StdCallbackContext> This Encapsulates the actual Call to the service that is being made via caller.CallChain.Completed<RequestT,ResponseT,ClientT,ModelT,CallbackT extends StdCallbackContext> One the call sequence has completed successfully, this is called to provide the progress event.CallChain.Exceptional<RequestT,ResponseT,ClientT,ModelT,CallbackT extends StdCallbackContext> This provide the handler with the option to provide an explicit exception handler that would have service exceptions that was received.CallChain.ExceptionPropagate<RequestT,E extends Exception,ClientT,ModelT,CallbackT extends StdCallbackContext,ReturnT> When implementing this interface, developers can either propagate the exception as is.CallChain.Initiator<ClientT,ModelT,CallbackT extends StdCallbackContext> Provides an API initiator interface that works for all API calls that need conversion, retry-backoff strategy, common exception handling and more against desired state of the resource and callback context.CallChain.RequestMaker<ClientT,ModelT,CallbackT extends StdCallbackContext> This performs the translate step between the ModelT properties and what is needed for making the service call.CallChain.Stabilizer<RequestT,ResponseT,ClientT,ModelT,CallbackT extends StdCallbackContext> This provides an optional stabilization function to be incorporate before we are done with the actual web service request.Delay This interface defines theDelaythat you needed between invocations of a specific call chain.DelayFactory Logger ProxyClient<ClientT> This class provides a wrapper for the client and provides methods to inject scoped credentials for each request context when invoking AWS services. -
Class Summary Class Description AmazonWebServicesClientProxy This implements the proxying mechanism to inject appropriate scoped credentials into a service call when making Amazon Webservice calls.CloudFormationCallbackAdapter<T> Credentials HandlerRequest<ResourceT,CallbackT> This interface describes the request object for the provisioning requestHandlerResponse<ResourceT> This interface describes the response object for the provisioning requestLoggerProxy MetricsPublisherProxy ProgressEvent<ResourceT,CallbackT> RequestContext<CallbackT> RequestData<ResourceT> ResourceHandlerRequest<T> This interface describes the request object for the provisioning request passed to the implementor.ResourceHandlerTestPayload<ModelT,CallbackT> This POJO is for the test entrypoint that bypasses the wrapper for direct testing.StabilizationData StdCallbackContext StdCallbackContext provide a mechanism that automatically provides the memoization for retention and callback of request, responses, stabilize handles during handler invocations.StdCallbackContext.Deserializer StdCallbackContext.Serializer -
Enum Summary Enum Description HandlerErrorCode OperationStatus StabilizationMode