From d9e1744996567297fc448a9993c416b5b5706956 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Mon, 7 Apr 2025 14:33:32 +0200 Subject: [PATCH 1/6] adding new pattern : step functions to on-premises API --- .gitignore | 5 +- .../README.md | 82 +++++++++++++ .../architecture.png | Bin 0 -> 62610 bytes .../example-pattern.json | 59 ++++++++++ .../example.tfvars | 5 + .../main.tf | 33 ++++++ .../modules/eventbridge_connection/main.tf | 81 +++++++++++++ .../modules/eventbridge_connection/outputs.tf | 10 ++ .../eventbridge_connection/variables.tf | 31 +++++ .../modules/state_machine/main.tf | 108 ++++++++++++++++++ .../modules/state_machine/outputs.tf | 14 +++ .../state_machine/state-machine.asl.json | 38 ++++++ .../modules/state_machine/variables.tf | 20 ++++ .../outputs.tf | 9 ++ .../variables.tf | 31 +++++ 15 files changed, 524 insertions(+), 2 deletions(-) create mode 100644 stepfunctions-eventbridge-onpremise-tf/README.md create mode 100644 stepfunctions-eventbridge-onpremise-tf/architecture.png create mode 100644 stepfunctions-eventbridge-onpremise-tf/example-pattern.json create mode 100644 stepfunctions-eventbridge-onpremise-tf/example.tfvars create mode 100644 stepfunctions-eventbridge-onpremise-tf/main.tf create mode 100644 stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/main.tf create mode 100644 stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/outputs.tf create mode 100644 stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/variables.tf create mode 100644 stepfunctions-eventbridge-onpremise-tf/modules/state_machine/main.tf create mode 100644 stepfunctions-eventbridge-onpremise-tf/modules/state_machine/outputs.tf create mode 100644 stepfunctions-eventbridge-onpremise-tf/modules/state_machine/state-machine.asl.json create mode 100644 stepfunctions-eventbridge-onpremise-tf/modules/state_machine/variables.tf create mode 100644 stepfunctions-eventbridge-onpremise-tf/outputs.tf create mode 100644 stepfunctions-eventbridge-onpremise-tf/variables.tf diff --git a/.gitignore b/.gitignore index c9f43bf2d6..244a3ef8f0 100644 --- a/.gitignore +++ b/.gitignore @@ -206,11 +206,12 @@ crash.log crash.*.log # Exclude all .tfvars files, which are likely to contain sensitive data, such as -# password, private keys, and other secrets. These should not be part of version -# control as they are data points which are potentially sensitive and subject +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject # to change depending on the environment. *.tfvars *.tfvars.json +!stepfunctions-eventbridge-onpremise-tf/example.tfvars # Ignore override files as they are usually used to override resources locally and so # are not checked in diff --git a/stepfunctions-eventbridge-onpremise-tf/README.md b/stepfunctions-eventbridge-onpremise-tf/README.md new file mode 100644 index 0000000000..7023248cb8 --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/README.md @@ -0,0 +1,82 @@ +# AWS Step Functions to on-premises API + +This pattern demonstrate how to call an on-premises API from a Step Functions state machine, leveraging Amazon EventBridge connection and VPC Lattice resource + gateway and resource configuration. + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli?in=terraform/aws-get-started) installed + +## Deployment Instructions + +### Pre-requisites + +This example assumes you already have a VPC with a connection to your datacenter (through VPN or Direct Connect) and an API is exposed on-premises and accessible from this VPC. +The VPC and connection to your datacenter are not provided by this example. Refer to this [documentation](https://docs.aws.amazon.com/whitepapers/latest/aws-vpc-connectivity-options/network-to-amazon-vpc-connectivity-options.html) to set up such connectivity. + +### Deployment + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change directory to the pattern directory: + ``` + cd stepfunctions-eventbridge-onpremise-tf + ``` +3. Copy and edit `example.tfvars` with your custom values +4. From the command line, use Terraform to deploy the AWS resources: + ``` + terraform init + terraform apply -var-file=your-variables.tfvars + ``` + + When prompted do you want to deploy the infrastructure, type ```yes``` and press enter. + +5. Note the outputs from the terraform deployment process. These contain the resource ARNs which are used for testing. + +## How it works + +![Architecture](architecture.png) + +1. The HTTP task in Step Functions is leveraging an EventBridge Connection. It defines the target endpoint (e.g. https://my-internal-api.company.com/customer) and HTTP method (e.g. GET) as well as eventual HTTP headers. +2. The EventBridge Connection defines the authentication mechanism (OAuth, Basic or API Key in this case) for the target endpoint as well as the resource configuration to use for a private/internal endpoint. +3. The resource configuration defines the target endpoint itself, generally an on-premise IP address or DNS name (e.g. my-internal-api.company.com). Resource configuration is associated to a resource gateway. +4. The resource gateway "opens a door" to the VPC and allow ingress. It is linked to the chosen subnets (generally private) and is also protected by a security group to further protect your backend API. Note: You could stop here at the VPC level, with a private API deployed in a private subnet. +5. The site-to-site VPN or Direct Connect connection establishes the connection between the AWS cloud (generally with a VPN Gateway or a Transit Gateway) and your datacenter (through a Customer Gateway). +6. Finally, the internal API that resides in your datacenter can be accessed via this "route". + +You can get more details in this [blog post](https://community.aws/content/2oExiwtkpK7go3wzAVzzF05ysqu). + +## Testing + +1. First make sure the EventBridge connection is active. Use the command `aws events describe-connection --name on-premise-connection --query ConnectionState` and verify it is `ACTIVE`. Otherwise, wait for an additional minute and verify again. +2. Go to the AWS Step Functions console and open the state machine deployed by the example (`state-machine-call-onprem`). +3. Click on `Start Execution` on the top right and again in the popup (no input is required for this example). +4. Observe the result. Your on-premise API should be called by the state machine and an eventual result returned to the task. + +You can also use the AWS CLI with the following command (make sure to use the output of the terraform script): + +```shell + aws stepfunctions start-execution --state-machine-arn arn:aws:states:us-east-1:123456789012:stateMachine:state-machine-call-onprem +``` + +## Cleanup +**To avoid incurring future charges, delete the resources created by the Terraform script.** +1. Return to the directory where you deployed your terraform script. +2. To destroy the infrastructure in AWS, run the command + +```bash + terraform destroy +``` + When prompted do you want to destroy the infrastructure, type ```yes``` and press enter. + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/stepfunctions-eventbridge-onpremise-tf/architecture.png b/stepfunctions-eventbridge-onpremise-tf/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..2d97cf07eea94cb3ff5d725d044851de41e44ac9 GIT binary patch literal 62610 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 Cg!fed literal 0 HcmV?d00001 diff --git a/stepfunctions-eventbridge-onpremise-tf/example-pattern.json b/stepfunctions-eventbridge-onpremise-tf/example-pattern.json new file mode 100644 index 0000000000..74f99223eb --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/example-pattern.json @@ -0,0 +1,59 @@ +{ + "title": "Step Functions to on-premises API", + "description": "Step Functions performing HTTP call to on-premises API", + "language": "", + "level": "200", + "framework": "Terraform", + "introBox": { + "headline": "How it works", + "text": [ + "This sample project demonstrates how to use an AWS Step Functions state machine to call an on-premises API without using an intermediary Lambda function. This pattern is leveraging EventBridge connection to connect to an HTTP endpoint and VPC Lattice to access private resources (in a VPC or on premises).", + "This pattern deploys one Step Functions, one EventBridge connection, a VPC Lattice resource configuration and resource gateway. You need to have a connection between a VPC and a datacenter (using VPN or Direct Connect)." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/stepfunctions-eventbridge-onpremise-tf", + "templateURL": "serverless-patterns/stepfunctions-eventbridge-onpremise-tf", + "projectFolder": "stepfunctions-eventbridge-onpremise-tf", + "templateFile": "main.tf" + } + }, + "resources": { + "bullets": [ + { + "text": "Connect to private APIs using EventBridge connections", + "link": "https://docs.aws.amazon.com/eventbridge/latest/userguide/connection-private.html" + }, + { + "text": "Call HTTPs endpoints using Step Functions HTTP Task", + "link": "https://docs.aws.amazon.com/step-functions/latest/dg/call-https-apis.html" + } + ] + }, + "deploy": { + "text": [ + "terraform init", + "terraform apply" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "terraform destroy" + ] + }, + "authors": [ + { + "name": "Jerome Van Der Linden", + "image": "https://serverlessland.com/assets/images/resources/contributors/jerome-van-der-linden.jpg", + "bio": "Jerome is a Solutions Architect Builder at AWS. Passionate about building stuff using the AWS services, and especially the serverless ones.", + "linkedin": "jeromevdl", + "twitter": "jeromevdl" + } + ] +} diff --git a/stepfunctions-eventbridge-onpremise-tf/example.tfvars b/stepfunctions-eventbridge-onpremise-tf/example.tfvars new file mode 100644 index 0000000000..f71a209b72 --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/example.tfvars @@ -0,0 +1,5 @@ +api_domain_name = "api.internal.mycompany.com" +api_key_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:07a4e645-fc95-4a10-853a-410b1b1eca5b-012nZO" +vpc_id = "vpc-0e03d4ab114e951be" +on_premises_cidr = "172.32.0.0/20" +private_subnet_ids = ["subnet-05d53fa850148290e","subnet-070324fd8bc5885a5"] \ No newline at end of file diff --git a/stepfunctions-eventbridge-onpremise-tf/main.tf b/stepfunctions-eventbridge-onpremise-tf/main.tf new file mode 100644 index 0000000000..caaf82519a --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/main.tf @@ -0,0 +1,33 @@ +terraform { + required_version = ">= 1.0.0" # Ensure that the Terraform version is 1.0.0 or higher + + required_providers { + aws = { + source = "hashicorp/aws" # Specify the source of the AWS provider + version = ">= 5.0.0" # Use a version of the AWS provider that is compatible with version + } + } +} + +provider "aws" { + region = "us-east-1" +} + +module "eventbridge_connection" { + source = "./modules/eventbridge_connection" + + vpc_id = var.vpc_id + on_premises_cidr = var.on_premises_cidr + api_domain_name = var.api_domain_name + private_subnet_ids = var.private_subnet_ids + api_key_secret_arn = var.api_key_secret_arn +} + +module "state_machine" { + source = "./modules/state_machine" + + connection_arn = module.eventbridge_connection.connection_arn + connection_secret_arn = module.eventbridge_connection.connection_secret_arn + api_domain_name = var.api_domain_name + log_retention_days = 30 +} diff --git a/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/main.tf b/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/main.tf new file mode 100644 index 0000000000..31c80b910f --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/main.tf @@ -0,0 +1,81 @@ +resource "aws_security_group" "resource_gateway_sg" { + name_prefix = "resource-gateway-sg" + description = "Security group for resource gateway" + vpc_id = var.vpc_id + + egress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = [var.on_premises_cidr] + description = "Allow HTTPS traffic to on-premises" + } + + egress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [var.on_premises_cidr] + description = "Allow HTTP traffic to on-premises" + } +} + +resource "aws_vpclattice_resource_gateway" "on_premise_resource_gateway" { + name = "resource-gateway" + ip_address_type = "IPV4" + vpc_id = var.vpc_id + security_group_ids = [aws_security_group.resource_gateway_sg.id] + subnet_ids = var.private_subnet_ids +} + +resource "aws_vpclattice_resource_configuration" "on_premise_resource_configuration" { + name = "resource-config" + port_ranges = ["80", "443"] + protocol = "TCP" + resource_gateway_identifier = aws_vpclattice_resource_gateway.on_premise_resource_gateway.id + type = "SINGLE" + + resource_configuration_definition { + # uncomment if using ip address + # ip_resource { + # ip_address = var.api_ip_address + # } + + # remove if using ip address + dns_resource { + domain_name = var.api_domain_name + ip_address_type = "IPV4" + } + } +} + +data "aws_secretsmanager_secret_version" "api_key" { + secret_id = var.api_key_secret_arn +} + +resource "aws_cloudwatch_event_connection" "on_premise_connection" { + name = "on-premise-connection" + description = "Connection to on premises API" + authorization_type = "API_KEY" + + auth_parameters { + # configure basic or oauth instead of api_key, depending on your authentication method + api_key { + key = "x-api-key" + value = data.aws_secretsmanager_secret_version.api_key.secret_string + } + # eventually add http parameters (header, body or query string) to the connection + invocation_http_parameters { + header { + key = "x-origin" + value = "aws-state-machine" + } + } + } + + invocation_connectivity_parameters { + resource_parameters { + resource_configuration_arn = aws_vpclattice_resource_configuration.on_premise_resource_configuration.arn + } + } +} diff --git a/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/outputs.tf b/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/outputs.tf new file mode 100644 index 0000000000..7af93d6e84 --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/outputs.tf @@ -0,0 +1,10 @@ +output "connection_arn" { + description = "ARN of the EventBridge connection" + value = aws_cloudwatch_event_connection.on_premise_connection.arn +} + +output "connection_secret_arn" { + description = "ARN of the secret for EventBridge connection" + value = aws_cloudwatch_event_connection.on_premise_connection.secret_arn +} + diff --git a/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/variables.tf b/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/variables.tf new file mode 100644 index 0000000000..075b31c96d --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/modules/eventbridge_connection/variables.tf @@ -0,0 +1,31 @@ +variable "vpc_id" { + type = string + description = "ID of the VPC linked to on-premises network" +} + +variable "private_subnet_ids" { + type = list(string) + description = "List of private subnet IDs in the VPC" +} + +variable "on_premises_cidr" { + type = string + description = "CIDR block of the on-premises network" +} + +# Choose either a domain name if you have one configured for your API, or and IP address +variable "api_domain_name" { + type = string + description = "Domain name of the on-premises API" +} + +# variable "api_ip_address" { +# type = string +# description = "IP address of the on-premises API" +# } + +# If using Basic or OAuth, change the authentication mechanism +variable "api_key_secret_arn" { + type = string + description = "ARN of the existing secret containing the API key" +} \ No newline at end of file diff --git a/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/main.tf b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/main.tf new file mode 100644 index 0000000000..91fd33a0c7 --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/main.tf @@ -0,0 +1,108 @@ + +# modules/state_machine/main.tf +resource "aws_iam_role" "state_machine" { + name = "state-machine-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "states.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy" "state_machine" { + name = "state-machine-policy" + role = aws_iam_role.state_machine.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "invokeHTTP" + Effect = "Allow" + Action = "states:InvokeHTTPEndpoint" + Resource = "*" + Condition = { + StringLike = { + "states:HTTPEndpoint" = "https://${var.api_domain_name}/*" + } + } + }, + { + Sid = "retrieveEBConnection" + Effect = "Allow" + Action = "events:RetrieveConnectionCredentials" + Resource = var.connection_arn + }, + { + Sid = "retrieveEBSecretForConnection" + Effect = "Allow" + Action = [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ] + Resource = var.connection_secret_arn + } + ] + }) +} + +resource "aws_cloudwatch_log_group" "state_machine" { + name = "/aws/stepfunctions/state-machine" + retention_in_days = var.log_retention_days +} + +resource "aws_iam_role_policy" "state_machine_logging" { + name = "state-machine-logging-policy" + role = aws_iam_role.state_machine.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogDelivery", + "logs:GetLogDelivery", + "logs:UpdateLogDelivery", + "logs:DeleteLogDelivery", + "logs:ListLogDeliveries", + "logs:PutLogEvents", + "logs:PutResourcePolicy", + "logs:DescribeResourcePolicies", + "logs:DescribeLogGroups" + ] + Resource = "*" + } + ] + }) +} + +resource "aws_sfn_state_machine" "state_machine" { + name = "state-machine-call-onprem" + role_arn = aws_iam_role.state_machine.arn + + definition = templatefile("${path.module}/state-machine.asl.json", { + EventBridgeConnectionArn = var.connection_arn + GetHelloEndpoint = "https://${var.api_domain_name}/hello" + }) + + logging_configuration { + log_destination = "${aws_cloudwatch_log_group.state_machine.arn}:*" + include_execution_data = true + level = "ALL" + } + + tracing_configuration { + enabled = true + } + + type = "EXPRESS" +} diff --git a/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/outputs.tf b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/outputs.tf new file mode 100644 index 0000000000..591bcad3b8 --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/outputs.tf @@ -0,0 +1,14 @@ +output "state_machine_arn" { + description = "ARN of the Step Functions state machine" + value = aws_sfn_state_machine.state_machine.arn +} + +output "role_arn" { + description = "ARN of the IAM role used by the state machine" + value = aws_iam_role.state_machine.arn +} + +output "log_group_name" { + description = "Name of the CloudWatch Log Group for the state machine" + value = aws_cloudwatch_log_group.state_machine.name +} diff --git a/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/state-machine.asl.json b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/state-machine.asl.json new file mode 100644 index 0000000000..5fb2e3133e --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/state-machine.asl.json @@ -0,0 +1,38 @@ +{ +"QueryLanguage": "JSONata", +"Comment": "Call On-Premises API", + "StartAt": "DirectHTTPCall", + "States": { + "DirectHTTPCall": { + "Type": "Task", + "Resource": "arn:aws:states:::http:invoke", + "Arguments": { + "ApiEndpoint": "${GetHelloEndpoint}", + "Method": "GET", + "InvocationConfig": { + "ConnectionArn": "${EventBridgeConnectionArn}" + }, + "Headers": { + "x-execution-id": "{% $states.context.Execution.Name %}", + "x-state-machine": "{% $states.context.StateMachine.Name %}" + } + }, + "Catch": [ + { + "ErrorEquals": [ + "States.ALL" + ], + "Next": "HandleError" + } + ], + "Next": "Success" + }, + "HandleError": { + "Type": "Fail", + "Cause": "Direct API call failed" + }, + "Success": { + "Type": "Succeed" + } + } +} \ No newline at end of file diff --git a/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/variables.tf b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/variables.tf new file mode 100644 index 0000000000..89c671b676 --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/variables.tf @@ -0,0 +1,20 @@ +variable "connection_arn" { + type = string + description = "ARN of the EventBridge connection" +} + +variable "connection_secret_arn" { + type = string + description = "ARN of the secret for EventBridge connection" +} + +variable "api_domain_name" { + type = string + description = "Domain name for the API" +} + +variable "log_retention_days" { + type = number + description = "Number of days to retain logs" + default = 30 +} \ No newline at end of file diff --git a/stepfunctions-eventbridge-onpremise-tf/outputs.tf b/stepfunctions-eventbridge-onpremise-tf/outputs.tf new file mode 100644 index 0000000000..2ad7f83720 --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/outputs.tf @@ -0,0 +1,9 @@ +output "connection_arn" { + description = "ARN of the EventBridge connection" + value = module.eventbridge_connection.connection_arn +} + +output "state_machine_arn" { + description = "ARN of the Step Functions state machine" + value = module.state_machine.state_machine_arn +} \ No newline at end of file diff --git a/stepfunctions-eventbridge-onpremise-tf/variables.tf b/stepfunctions-eventbridge-onpremise-tf/variables.tf new file mode 100644 index 0000000000..075b31c96d --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/variables.tf @@ -0,0 +1,31 @@ +variable "vpc_id" { + type = string + description = "ID of the VPC linked to on-premises network" +} + +variable "private_subnet_ids" { + type = list(string) + description = "List of private subnet IDs in the VPC" +} + +variable "on_premises_cidr" { + type = string + description = "CIDR block of the on-premises network" +} + +# Choose either a domain name if you have one configured for your API, or and IP address +variable "api_domain_name" { + type = string + description = "Domain name of the on-premises API" +} + +# variable "api_ip_address" { +# type = string +# description = "IP address of the on-premises API" +# } + +# If using Basic or OAuth, change the authentication mechanism +variable "api_key_secret_arn" { + type = string + description = "ARN of the existing secret containing the API key" +} \ No newline at end of file From 07c04522d9c3644e7d329711828c2ef630ff84e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:20:25 +0200 Subject: [PATCH 2/6] Update state-machine.asl.json remove unnecessary headers --- .../modules/state_machine/state-machine.asl.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/state-machine.asl.json b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/state-machine.asl.json index 5fb2e3133e..bdbe2f0d3c 100644 --- a/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/state-machine.asl.json +++ b/stepfunctions-eventbridge-onpremise-tf/modules/state_machine/state-machine.asl.json @@ -11,10 +11,6 @@ "Method": "GET", "InvocationConfig": { "ConnectionArn": "${EventBridgeConnectionArn}" - }, - "Headers": { - "x-execution-id": "{% $states.context.Execution.Name %}", - "x-state-machine": "{% $states.context.StateMachine.Name %}" } }, "Catch": [ @@ -35,4 +31,4 @@ "Type": "Succeed" } } -} \ No newline at end of file +} From dd9ef7c7ff1eebfd8b1564aec8e99adc9ebff1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:21:09 +0200 Subject: [PATCH 3/6] Update README.md --- stepfunctions-eventbridge-onpremise-tf/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/stepfunctions-eventbridge-onpremise-tf/README.md b/stepfunctions-eventbridge-onpremise-tf/README.md index 7023248cb8..72acfcc462 100644 --- a/stepfunctions-eventbridge-onpremise-tf/README.md +++ b/stepfunctions-eventbridge-onpremise-tf/README.md @@ -1,7 +1,6 @@ -# AWS Step Functions to on-premises API +# AWS Step Functions to on-premises API (Terraform) -This pattern demonstrate how to call an on-premises API from a Step Functions state machine, leveraging Amazon EventBridge connection and VPC Lattice resource - gateway and resource configuration. +This pattern demonstrate how to call an on-premises API from a Step Functions state machine, leveraging Amazon EventBridge connection and VPC Lattice resource gateway and resource configuration. Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. From 4dcf75b83658aec96b532bfbe242ccbfdf6a8bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Van=20Der=20Linden?= <117538+jeromevdl@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:22:19 +0200 Subject: [PATCH 4/6] Update example-pattern.json --- stepfunctions-eventbridge-onpremise-tf/example-pattern.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stepfunctions-eventbridge-onpremise-tf/example-pattern.json b/stepfunctions-eventbridge-onpremise-tf/example-pattern.json index 74f99223eb..4387e2203b 100644 --- a/stepfunctions-eventbridge-onpremise-tf/example-pattern.json +++ b/stepfunctions-eventbridge-onpremise-tf/example-pattern.json @@ -1,5 +1,5 @@ { - "title": "Step Functions to on-premises API", + "title": "Step Functions to on-premises API (Terraform)", "description": "Step Functions performing HTTP call to on-premises API", "language": "", "level": "200", From eed10a031c62a7413f4161f79e4af9ad4ac88914 Mon Sep 17 00:00:00 2001 From: Udit Parikh Date: Sun, 13 Jul 2025 18:07:00 +0530 Subject: [PATCH 5/6] Create stepfunctions-eventbridge-onpremise-tf.json --- ...tepfunctions-eventbridge-onpremise-tf.json | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 stepfunctions-eventbridge-onpremise-tf/stepfunctions-eventbridge-onpremise-tf.json diff --git a/stepfunctions-eventbridge-onpremise-tf/stepfunctions-eventbridge-onpremise-tf.json b/stepfunctions-eventbridge-onpremise-tf/stepfunctions-eventbridge-onpremise-tf.json new file mode 100644 index 0000000000..8bb1ad1043 --- /dev/null +++ b/stepfunctions-eventbridge-onpremise-tf/stepfunctions-eventbridge-onpremise-tf.json @@ -0,0 +1,97 @@ +{ + "title": "Step Functions to on-premises API (Terraform)", + "description": "Step Functions performing HTTP call to on-premises API", + "language": "", + "level": "200", + "framework": "Terraform", + "introBox": { + "headline": "How it works", + "text": [ + "This sample project demonstrates how to use an AWS Step Functions state machine to call an on-premises API without using an intermediary Lambda function. This pattern is leveraging EventBridge connection to connect to an HTTP endpoint and VPC Lattice to access private resources (in a VPC or on premises).", + "This pattern deploys one Step Functions, one EventBridge connection, a VPC Lattice resource configuration and resource gateway. You need to have a connection between a VPC and a datacenter (using VPN or Direct Connect)." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/stepfunctions-eventbridge-onpremise-tf", + "templateURL": "serverless-patterns/stepfunctions-eventbridge-onpremise-tf", + "projectFolder": "stepfunctions-eventbridge-onpremise-tf", + "templateFile": "main.tf" + } + }, + "resources": { + "bullets": [ + { + "text": "Connect to private APIs using EventBridge connections", + "link": "https://docs.aws.amazon.com/eventbridge/latest/userguide/connection-private.html" + }, + { + "text": "Call HTTPs endpoints using Step Functions HTTP Task", + "link": "https://docs.aws.amazon.com/step-functions/latest/dg/call-https-apis.html" + } + ] + }, + "deploy": { + "text": [ + "terraform init", + "terraform apply" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "terraform destroy" + ] + }, + "authors": [ + { + "name": "Jerome Van Der Linden", + "image": "https://serverlessland.com/assets/images/resources/contributors/jerome-van-der-linden.jpg", + "bio": "Jerome is a Solutions Architect Builder at AWS. Passionate about building stuff using the AWS services, and especially the serverless ones.", + "linkedin": "jeromevdl", + "twitter": "jeromevdl" + } + ], + "patternArch": { + "icon1": { + "x": 15, + "y": 50, + "service": "sfn", + "label": "AWS Step Functions" + }, + "icon2": { + "x": 40, + "y": 50, + "service": "eventbridge", + "label": "Amazon EventBridge" + }, + "icon3": { + "x": 65, + "y": 50, + "service": "vpc-lattice", + "label": "Amazon VPC Lattice" + }, + "icon4": { + "x": 90, + "y": 50, + "service": "vpc", + "label": "Amazon VPC" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "line2": { + "from": "icon2", + "to": "icon3" + }, + "line3": { + "from": "icon3", + "to": "icon4" + } + } +} From 57ea0c117aa4b222f6bc2f7681062daf19d8b0a3 Mon Sep 17 00:00:00 2001 From: Jerome Van Der Linden Date: Mon, 22 Sep 2025 13:09:21 +0200 Subject: [PATCH 6/6] move tfvars example in README --- .gitignore | 1 - stepfunctions-eventbridge-onpremise-tf/README.md | 13 +++++++++++-- .../example.tfvars | 5 ----- 3 files changed, 11 insertions(+), 8 deletions(-) delete mode 100644 stepfunctions-eventbridge-onpremise-tf/example.tfvars diff --git a/.gitignore b/.gitignore index 244a3ef8f0..f1e3d74228 100644 --- a/.gitignore +++ b/.gitignore @@ -211,7 +211,6 @@ crash.*.log # to change depending on the environment. *.tfvars *.tfvars.json -!stepfunctions-eventbridge-onpremise-tf/example.tfvars # Ignore override files as they are usually used to override resources locally and so # are not checked in diff --git a/stepfunctions-eventbridge-onpremise-tf/README.md b/stepfunctions-eventbridge-onpremise-tf/README.md index 72acfcc462..2b3c788b10 100644 --- a/stepfunctions-eventbridge-onpremise-tf/README.md +++ b/stepfunctions-eventbridge-onpremise-tf/README.md @@ -28,14 +28,23 @@ The VPC and connection to your datacenter are not provided by this example. Refe ``` cd stepfunctions-eventbridge-onpremise-tf ``` -3. Copy and edit `example.tfvars` with your custom values +3. Create a `.tfvars` file with the following variables (use your custom values) + + ``` + api_domain_name = "api.internal.mycompany.com" + api_key_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:07a4e645-fc95-4a10-853a-410b1b1eca5b-012nZO" + vpc_id = "vpc-0e03d4ab114e951be" + private_subnet_ids = ["subnet-05d53fa850148290e","subnet-070324fd8bc5885a5"] + on_premises_cidr = "172.32.0.0/20" + ``` + 4. From the command line, use Terraform to deploy the AWS resources: ``` terraform init terraform apply -var-file=your-variables.tfvars ``` - When prompted do you want to deploy the infrastructure, type ```yes``` and press enter. + When prompted "Do you want to deploy the infrastructure", type ```yes``` and press enter. 5. Note the outputs from the terraform deployment process. These contain the resource ARNs which are used for testing. diff --git a/stepfunctions-eventbridge-onpremise-tf/example.tfvars b/stepfunctions-eventbridge-onpremise-tf/example.tfvars deleted file mode 100644 index f71a209b72..0000000000 --- a/stepfunctions-eventbridge-onpremise-tf/example.tfvars +++ /dev/null @@ -1,5 +0,0 @@ -api_domain_name = "api.internal.mycompany.com" -api_key_secret_arn = "arn:aws:secretsmanager:us-east-1:123456789012:secret:07a4e645-fc95-4a10-853a-410b1b1eca5b-012nZO" -vpc_id = "vpc-0e03d4ab114e951be" -on_premises_cidr = "172.32.0.0/20" -private_subnet_ids = ["subnet-05d53fa850148290e","subnet-070324fd8bc5885a5"] \ No newline at end of file