Refactoring a serverless application to use Step Functions third-party API call integration
Tags: aws, serverless, stepfunctions
Published: 2023-12-14 07:16:09

With the new third-party API invoke feature in Step Functions, we might not need a Lambda function to invoke external HTTPS endpoints. When we remove it, the state machine will become smaller and easier to maintain, and our logic might run faster.

## 1. Weather application - Version 1

I recently created a weather application for a demo that collects and displays temperature data at my location.

I use the <a href="https://openweathermap.org/" class="link" target="_blank" rel="noopener noreferrer">OpenWeather API</a> to receive the current temperature data by providing the latitude and longitude coordinates as query strings in the request. A <a href="https://aws.amazon.com/lambda" class="link" target="_blank" rel="noopener noreferrer">Lambda</a> function invokes the weather API and sends the current temperature value as a **custom metric** to <a href="https://aws.amazon.com/cloudwatch/" class="link" target="_blank" rel="noopener noreferrer">CloudWatch</a>. I then graph the temperature values on a **dashboard** and activate an alarm when the temperature sinks below zero degrees Celsius.

I use <a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/using-eventbridge-scheduler.html" class="link" target="_blank" rel="noopener noreferrer">EventBridge Scheduler</a> to run a cron job and trigger the Lambda function every X minutes.

I store the API key as a **secure string** in <a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" class="link" target="_blank" rel="noopener noreferrer">Systems Manager Parameter Store</a>. The function calls the `GetParameter` API to receive the secret at runtime and stores it in memory. This way, we can reduce the number of calls to Parameter Store and **reuse the execution environment** as long as possible.

![Existing X-Ray service map](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ge97kpxb3xp5nw5ibg29.png)

I also installed <a href="https://docs.powertools.aws.dev/lambda/typescript/latest/" class="link" target="_blank" rel="noopener noreferrer">Powertools for AWS Lambda</a> as a dependency. It allows me to easily add the custom `Temperature` metric as an <a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Embedded_Metric_Format_Specification.html" class="link" target="_blank" rel="noopener noreferrer">embedded metric format</a>, so that CloudWatch can extract the metric data from the log events.

I also send some annotations and subsegments to <a href="https://aws.amazon.com/xray/" class="link" target="_blank" rel="noopener noreferrer">X-Ray</a> that makes it easy to identify bottlenecks and Lambda cold starts.

## 2. New Step Functions features

I had been happy with my new mini-application when AWS <a href="https://aws.amazon.com/about-aws/whats-new/2023/11/aws-step-functions-https-endpoints-teststate-api/" class="link" target="_blank" rel="noopener noreferrer">announced</a> support for direct HTTPS endpoint invocations in <a href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html" class="link" target="_blank" rel="noopener noreferrer">Step Functions</a> at re:Invent 2023. With this new feature, we might not need any Lambdas that call the 3rd party API in the state machine! I wanted to see if I could refactor the application and remove the function from the workflow while keeping all the other features.

## 3. About the Call third-party API task

Let us see how the direct third-party HTTP endpoint integration works at a high level.

Step Functions uses the same API as <a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html" class="link" target="_blank" rel="noopener noreferrer">EventBridge API destinations</a> to call HTTP endpoints. We should follow the same procedure as if we set up an HTTP target for a rule in EventBridge.

So we create a <a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html#eb-api-destination-connection" class="link" target="_blank" rel="noopener noreferrer">connection</a> first and specify authorization pattern, which is API key in this case. EventBridge Connections uses <a href="https://aws.amazon.com/secrets-manager/" class="link" target="_blank" rel="noopener noreferrer">Secrets Manager</a> to store the key.

![The new state](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tv0rds062r1bapwllgte.png)

The next step is to head over to the Step Functions page, select the `Call third-party API` task from the left menu in **Workflow Studio**, and drop it onto the canvas. We configure the endpoint, method, applicable query strings, and body payload. Incorporating the new state works like adding other `Task`s to the state machine.

## 4. Weather application - Version 2

So I started redesigning the application with Step Functions with the third-party API invoke feature.

### 4.1. The new state machine

A good practice when designing serverless applications is to use Lambda to **transform and not transport** and integrate services directly to each other whenever possible. The `Call third-party API` state does that with external APIs.

I also wanted to keep sending the `Temperature` custom metric to CloudWatch, so I added a `CloudWatch:PutMetricData` `Task` to the state machine.

The state machine has become very simple, and it looks like this:

![The new state machine](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ccvxe90we2boex9wpj9j.png)

### 4.2. The Call third-party API state

Next, we investigate the **Amazon State Language (ASL)** code for the `Task` states.

Here is the structure of the `Call third-party API` state:

```json
"OpenWeather API": {
  "Type": "Task",
  "Resource": "arn:aws:states:::http:invoke",
  "Parameters": {
    "ApiEndpoint": "https://api.openweathermap.org/data/2.5/weather",
    "Method": "GET",
    "Authentication": {
      "ConnectionArn": "arn:aws:events:eu-central-1:ACCOUNT_ID:connection/OpenWeatherMapApi/CONNECTION_ID"
    },
    "QueryParameters": {
      "lat": "LATITUDE COORDINATE",
      "lon": "LONGITUDE COORDINATE",
      "units": "metric"
    }
  },
  "Next": "Pass",
  "ResultSelector": {
    "temperature.$": "$.ResponseBody.main.temp"
  }
}
```

We can see that the `Parameters` object contains all the data that Step Functions needs to invoke the OpenWeather API. We should specify the API endpoint, the `Method`, and the query strings. The `Authentication` object references the EventBridge Connection that adds the API key to the call. Step Functions wraps the response in the `ResponseBody` object.

The OpenWeather API returns a large object with different weather data for the given location. However, I decided to keep only the temperature because I only need that for my CloudWatch dashboard. We can use `ResultSelector` in Step Functions to extract properties from the Task result. In this case, we create a new object where the key is `temperature` and the value is the current temperature returned from the API.

The output of the state will look like this:

```json
{
  "temperature": 2.4
}
```

We can apply this optimization technique because we need only the temperature value in the next state.

### 4.3. The PutMetricData state

Because I have removed the Lambda function from the logic, I needed to add another state that pushes the temperature to CloudWatch. Step Functions directly SDK-integrates with more than 10,000 AWS APIs. Luckily, `PutMetricData` is one of them, so we can use it to send the temperature data to the dashboard.

The `Task` state looks like this:

```json
"PutMetricData": {
  "Type": "Task",
  "Parameters": {
    "MetricData": [
      {
        "MetricName": "Temperature",
        "Unit": "Count",
        "Value.$": "$.temperature",
        "Dimensions": [
          {
            "Name": "CurrentWeather",
            "Value": "temperature"
          }
        ]
      }
    ],
    "Namespace": "StepFnWeatherApp"
  },
  "Resource": "arn:aws:states:::aws-sdk:cloudwatch:putMetricData",
  "Next": "Success"
}
```

The most important part of this definition is the `MetricData` array, where we can define the specifications of the given metric. We must append the `$` to the `Value` key because we extract its value dynamically from the input.

### 4.4. No code!

The best part of refactoring the application is that I don't have to write a single line of application code!

Step Function can handle retries and errors, so we could eliminate the `if/else` and `try/catch` blocks earlier from our code. With the new third-party API invoke state, we can delegate another task to Step Functions!

## 5. Testing the application

The other Step Functions announcement in re:Invent 2023 was the <a href="https://docs.aws.amazon.com/step-functions/latest/dg/test-state-isolation.html" class="link" target="_blank" rel="noopener noreferrer">TestState API</a>. We can test each state in isolation without executing the entire state machine using the new API.

TestState API brings us closer to the step-by-step `console.log`-style development. We can add the state to the final workflow if we are confident it works. It's a great feature and provides a different way to develop applications with Step Functions and create state machines in the Workflow Studio.

## 6. Considerations

I created the application with both Standard and Express workflows.

In my case, the **Standard** workflow ran in about 1 second, but the **Express** workflow was inconsistent due to the execution history overhead. It sometimes needed around 0.3-0.4 seconds to run but every 3rd-4th execution took about 4 seconds.

Comparing them to version 1 (~2 seconds with a Lambda cold start and ~1 second with a warm start), I experienced slightly faster execution times with the Standard workflow. I can save the **cold start** time, which, although not critical for me, can be an advantage in some use cases.

Another important factor when choosing the architecture is **cost**. According to the <a href="https://calculator.aws/#/" class="link" target="_blank" rel="noopener noreferrer">AWS Calculator</a>, I would pay $0.33 for the Standard workflow (including the 4000 free state transitions per month) and $0 for the Express (not enough invocations to make it billable) and for Version 1 with Lambda (the free tier covers it).

In my case (2880 invocations per month, six state changes per invocation), there is not much difference in the cost. Every use case is different, so you should evaluate the situation and create a solution that best fits your business needs.

## 7. Summary

Step Functions direct 3rd party HTTP integration adds a new tool to our serverless developer's toolbox. From now we don't have to use Lambda functions to invoke external endpoints, which might result in faster execution times and simpler architecture.

We can also test our states separately without executing the entire state machine. This way, we can ensure that the task we add to the state machine returns the expected result before incorporating it into the workflow.

## 8. Further reading

<a href="https://docs.aws.amazon.com/step-functions/latest/dg/connect-third-party-apis.html" class="link" target="_blank" rel="noopener noreferrer">Call third-party APIs</a> - Setting up direct 3rd party calls in Step Functions

<a href="https://docs.aws.amazon.com/step-functions/latest/dg/workflow-studio.html" class="link" target="_blank" rel="noopener noreferrer">Workflow Studio</a> - Everything about the Workflow Studio

<a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/publishingMetrics.html" class="link" target="_blank" rel="noopener noreferrer">Publish custom metrics</a> - Adding custom metrics to CloudWatch

<a href="https://docs.aws.amazon.com/step-functions/latest/dg/concepts-standard-vs-express.html" class="link" target="_blank" rel="noopener noreferrer">Standard vs. Express Workflows</a> - Comparison between the workflow types. When to choose which?

<a href="https://aws.amazon.com/blogs/developer/handling-errors-retries-and-adding-alerting-to-step-function-state-machine-executions/" class="link" target="_blank" rel="noopener noreferrer">Handling Errors, Retries, and adding Alerting to Step Function State Machine Executions</a> - Great AWS-post on error handling