AWS Database Blog

Migrate CLR code to AWS Lambda when upgrading Amazon RDS for SQL Server to 2025

In Addressing CLR assembly deprecation in Amazon RDS for SQL Server, we explained that Microsoft SQL Server 2016 reached end of extended support on July 14, 2026, and that Amazon Relational Database Service (Amazon RDS) for SQL Server 2017 and later doesn’t support user-defined SQL Common Language Runtime (CLR) assemblies. This is because CLR strict security treats every assembly as UNSAFE, and AWS doesn’t provide the sysadmin permissions needed to work around it on RDS for SQL Server.

That post presents four replacement strategies for your CLR code:

  • Option A – Upgrade to RDS for SQL Server 2025 and invoke AWS services (such as AWS Lambda) through the native sp_invoke_external_rest_endpoint stored procedure.
  • Option B – Convert to native Transact-SQL (T-SQL).
  • Option C – Move the logic to the application layer.
  • Option D – Migrate to SQL Server on Amazon Elastic Compute Cloud (Amazon EC2) (retains CLR).

This post is a hands-on walkthrough of Option A, for the cases where the CLR logic has no native T-SQL equivalent and you want to stay on fully managed Amazon RDS.

When Option A is the right choice

Option A is for the cases that have no native equivalent, where the CLR assembly does procedural work that T-SQL cannot express.

Problem

Consider a task where you need to load a delimited file where a single logical record can span multiple physical lines, because a quoted field contains an embedded line break, such as a multi-line address or a notes field. The file is RFC 4180 compliant, meaning a field wrapped in quotes is allowed to contain the row terminator. Native T-SQL ingestion tooling cannot parse this correctly, and there’s no built-in function that handles quote-aware, multi-line record boundaries.

Consider this record:

“AnyCompany, Inc.”,“123 Any St, Suite 1
Building A”,“Anytown”

That’s one logical row with three fields, but it spans two physical lines and the first field contains a comma. Native tooling gets this wrong:

  • STRING_SPLIT splits on every comma, so “AnyCompany, Inc.” becomes two columns.
  • BULK INSERT … FORMAT=‘CSV’ treats the embedded newline as a row terminator, so the single record is torn into two malformed rows. This is a documented limitation. The CSV format doesn’t support fields that contain the row terminator inside quotes.

That stateful parsing is exactly the kind of logic with no T-SQL equivalent, so it’s a legitimate candidate for Option A rather than an anti-pattern.

Solution

Because the parsing logic can’t be expressed in T-SQL but works fine as ordinary procedural code, the approach is to keep that logic intact and move it out of the database engine into a managed compute service that SQL Server can call. This preserves the exact quote-aware, multi-line behavior you already have while removing the CLR assembly and the sysadmin/UNSAFE privileges it required.

We use that CSV parser as the running example. Concretely, we:

  1. Move the parser’s core logic (unchanged) into an AWS Lambda function that accepts CSV text as JSON and returns the parsed rows as a JSON array.
  2. Expose the function through Amazon API Gateway with an API key and usage plan.
  3. Call it from SQL Server 2025 with sp_invoke_external_rest_endpoint, wrapped in a stored procedure that has the same signature as the original CLR function, so existing callers do not change.

The result is a fully managed workaround that parses the multi-line file correctly, with no CLR and no sysadmin access. All the code, including the Lambda project, the reference CLR assembly, and the SQL scripts, is in the accompanying repository on GitHub. You can take reference from the repository to modify your own CLR code and deploy as the solution described in this post.

Prerequisites

  • An Amazon RDS for SQL Server 2025 instance, with the external REST endpoint enabled parameter set to 1 in a custom parameter group. To modify a parameter in a parameter group, see Modifying parameters in a DB parameter group in Amazon RDS.If the RDS instance is in a private subnet, outbound internet access is required for sp_invoke_external_rest_endpoint. Provision a NAT gateway (in a public subnet with an internet gateway) and add a 0.0.0.0/0 route to the NAT in the RDS subnets’ route table. This keeps RDS private (egress-only) while allowing the outbound HTTPS call.
  • SQL Server Management Studio (SSMS) connected with the master user of RDS.
  • Familiarity with Git commands, .NET SDK and Amazon.Lambda.Tools, if you build the package yourself (the repository includes a prebuilt .zip).

Solution overview

The following flowchart shows the end-to-end path from the SQL Server stored procedure call through API Gateway and Lambda, back to T-SQL result rows by using OPENJSON(). The two highlighted nodes show that everything starts and ends in T-SQL. You keep the proven business logic from your CLR assembly and only rewrite the thin wrapper that connects it to the host. The SQL Server entry point becomes an AWS Lambda handler.

Flowchart of the request path from a SQL Server stored procedure through Amazon API Gateway and AWS Lambda and back to T-SQL result rows

Figure 1: End-to-end path from a SQL Server stored procedure call through Amazon API Gateway and AWS Lambda back to T-SQL result rows

The preceding diagram shows how to replace a legacy SQL CLR assembly with serverless architecture, because Amazon RDS for SQL Server doesn’t allow the custom CLR assemblies you’d run on premises. The flow:

1. Amazon RDS for SQL Server 2025 – Instead of calling into a CLR assembly inside the database, a stored procedure uses the native sp_invoke_external_rest_endpoint to make an outbound HTTPS call. The outbound HTTPS call originates from the RDS instance and, for a private-subnet RDS, egresses through a NAT gateway to the internet. The API Gateway sees the request arriving from the NAT gateway’s Elastic IP address (a stable public IP), which is what you allowlist in the API resource policy. Credentials and the API key are stored securely through a DATABASE SCOPED CREDENTIAL.

2. Amazon API Gateway – Receives that HTTPS POST, validates the API key against a usage plan, and forwards the request to Lambda using proxy integration.

3. AWS Lambda (.NET) – Runs the same parsing/business logic that used to live in the CLR assembly, now as managed .NET code outside the database. It returns its result as JSON.

4. T-SQL Result Set – SQL Server takes the JSON response back and parses it into rows and columns, so the caller gets a normal result set. The result looks just as if the old in-database CLR function had returned it.

In short: we’re moving custom .NET logic out of the database (where CLR isn’t supported on RDS) into a Lambda function reached through API Gateway, while keeping the SQL-side experience the same. You call a proc and get a result set back.

Note: The API key here is a long-lived static secret (stored encrypted in a DATABASE SCOPED CREDENTIAL, so it isn’t visible in the SQL). For production, consider an authentication method aligned to your security needs, such as API Gateway authorization that uses AWS Identity and Access Management (IAM) and short-lived SigV4 credentials rather than a static key.

Step 1: Build the Lambda deployment package

The accompanying repository contains the CsvParserLambda .NET project, a handler that reuses your CLR parsing logic and returns the parsed rows as JSON. Clone it and build the deployment package according to the steps in the repository README.md.

This step creates CsvParserLambda.zip, to be used in Step 2. For how to convert your own CLR .cs into a Lambda handler (what to keep and what to replace) and the full build/packaging details, see Migrating your own CLR code to AWS Lambda in the repository.

Step 2: Create the Lambda function

In this step, you create an execution role, attach the basic execution policy, and deploy the function from the ZIP package you built in Step 1.

2.1 Create the Lambda execution role

aws iam create-role \
    --role-name clr-csv-parser-role \
    --assume-role-policy-document '{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "lambda.amazonaws.com" },
    "Action": "sts:AssumeRole"
  }]
}'

2.2 Attach the basic execution policy (CloudWatch Logs)

aws iam attach-role-policy \
    --role-name clr-csv-parser-role \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

2.3 Create the function from the ZIP built in Step 1

aws lambda create-function \
    --function-name clr-csv-parser \
    --runtime dotnet10 \
    --architectures x86_64 \
    --handler "CsvParserLambda::CsvParserLambda.Function::FunctionHandler" \
    --timeout 60 \
    --memory-size 512 \
    --zip-file fileb://CsvParserLambda.zip \
    --role arn:aws:iam::<ACCOUNT_ID>:role/clr-csv-parser-role

Replace ACCOUNT_ID in the preceding command with the AWS account ID of your environment.

To verify the function in isolation, open the Test tab in the AWS Lambda console for the function clr-csv-parser created earlier and create an event that mimics an API Gateway proxy request:

{"body":"{\"skipHeader\":true,\"csvData\":\"CustomerName,Address,City\\n\\\"AnyCompany, Inc.\\\",\\\"123 Any St, Suite 1\\nBuilding A\\\",\\\"Anytown\\\"\",\"delimiter\":\",\"}"}

A successful response has “statusCode”: 200 and a body containing “success”:true with the parsed rows.

Everything the parser is meant to prove is working:

  • success: true, rowCount: 1 (header correctly skipped).
  • Field 1: AnyCompany, Inc. (comma inside quotes preserved as one field).
  • Field 2: 123 Any St, Suite 1\nBuilding A (the multi-line quoted field kept its internal newline, which is the whole reason this Lambda exists, because BULK INSERT can’t do this).
  • Field 3: Anytown.

Step 3: Create the REST API in API Gateway

Amazon API Gateway gives the Lambda function a secure, public HTTPS endpoint that SQL Server can call. This is necessary because sp_invoke_external_rest_endpoint requires an HTTPS URL with a publicly trusted TLS certificate. It cannot invoke a Lambda function directly.

We create a REST API with a single POST /parse method integrated with the parser function. It uses Lambda proxy integration, so the raw request body is passed straight through to the handler. This walkthrough uses a REGIONAL (public) API and we implement secure access to this regional API with a resource policy allowing only the NAT Elastic IP address.

3.1 Create the API

API_ID=$(aws apigateway create-rest-api \
    --name clr-csv-parser-api \
    --endpoint-configuration types=REGIONAL \
    --query 'id' --output text)

3.2 Get the root resource id (“/”)

ROOT_ID=$(aws apigateway get-resources \
    --rest-api-id "$API_ID" \
    --query 'items[?path==`/`].id' --output text)

3.3 Create the /parse resource

PARSE_ID=$(aws apigateway create-resource \
    --rest-api-id "$API_ID" \
    --parent-id "$ROOT_ID" \
    --path-part parse \
    --query 'id' --output text)

3.4 Create the POST method with API key required

aws apigateway put-method \
    --rest-api-id "$API_ID" \
    --resource-id "$PARSE_ID" \
    --http-method POST \
    --authorization-type NONE \
    --api-key-required

3.5 Wire POST to Lambda using proxy integration (AWS_PROXY)

REGION=$(aws configure get region)
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)

aws apigateway put-integration \
    --rest-api-id "$API_ID" \
    --resource-id "$PARSE_ID" \
    --http-method POST \
    --type AWS_PROXY \
    --integration-http-method POST \
    --uri "arn:aws:apigateway:${REGION}:lambda:path/2015-03-31/functions/arn:aws:lambda:${REGION}:${ACCOUNT_ID}:function:clr-csv-parser/invocations"

3.6 Grant API Gateway permission to invoke the Lambda

aws lambda add-permission \
    --function-name clr-csv-parser \
    --statement-id apigw-invoke \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --source-arn "arn:aws:execute-api:${REGION}:${ACCOUNT_ID}:${API_ID}/*/POST/parse"

3.7 Deploy to the “prod” stage

aws apigateway create-deployment \
    --rest-api-id "$API_ID" \
    --stage-name prod

3.8 Your endpoint is:

echo "https://${API_ID}.execute-api.${REGION}.amazonaws.com/prod/parse"

You will need the preceding output in Step 5.2, when we create the procedure in Amazon RDS for SQL Server.

3.9 Securing access to the API

The resource policy on the API can restrict by aws:SourceIp, the public IP that API Gateway sees the request arriving from. For sp_invoke_external_rest_endpoint, the outbound path depends on the subnet’s routing:

  • If RDS is in a public subnet (route table has a 0.0.0.0/0 route to an internet gateway), outbound traffic egresses using the instance’s own public IP (through the internet gateway).To get the RDS public IP address, use the nslookup command:
    nslookup <RDS Endpoint>
  • If RDS is in a private subnet and uses a NAT gateway, then outbound traffic to the internet goes through the NAT, so you allowlist the NAT Elastic IP address.To get the public Elastic IP address of the NAT gateway, check the Amazon Virtual Private Cloud (Amazon VPC) console or run the following command:
    NAT_IP=$(aws ec2 describe-nat-gateways \
        --nat-gateway-ids <YOUR_NAT_GATEWAY_ID> \
        --query 'NatGateways[0].NatGatewayAddresses[0].PublicIp' \
        --output text)
    echo $NAT_IP

Build the resource policy and attach it to the API through the console or update-rest-api, as described in Create and attach an API Gateway resource policy to an API.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:< REGION >:< ACCOUNT_ID >:<API_ID>/*",
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": "<RDS Public IP or NAT IP>/32"
        }
      }
    },
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "execute-api:Invoke",
      "Resource": "arn:aws:execute-api:< REGION >:< ACCOUNT_ID >:<API_ID>/*"
    }
  ]
}

After adding the resource policy, redeploy the API so the policy takes effect by running the deployment command from Step 3.7 again.

Step 4: Create an API key and usage plan

The API key authenticates calls from SQL Server, so the endpoint isn’t open to anonymous callers. The usage plan enforces throttling and quota limits to protect the Lambda function from runaway or excessive requests. When burst or rate limits are exceeded, API Gateway returns HTTP 429 “Too Many Requests” (or “Limit Exceeded” for quota) before the request reaches Lambda. T-SQL surfaces the same 429 error code. Together, they give you a managed access-control and rate-limiting layer in front of the parser. For more information, see Usage plans and API keys for REST APIs in API Gateway.

4.1 Create the API key

KEY_ID=$(aws apigateway create-api-key \
    --name clr-csv-parser-key \
    --enabled \
    --query 'id' --output text)

4.2 Retrieve the API key value

aws apigateway get-api-key \
    --api-key "$KEY_ID" \
    --include-value \
    --query 'value' --output text

4.3 Create a usage plan with throttling and quota, associated to the prod stage

PLAN_ID=$(aws apigateway create-usage-plan \
    --name clr-csv-parser-plan \
    --throttle burstLimit=50, rateLimit=100 \
    --quota limit=100000, period=MONTH \
    --api-stages "apiId=${API_ID},stage=prod" \
    --query 'id' --output text)

4.4 Associate the API key with the usage plan

aws apigateway create-usage-plan-key \
    --usage-plan-id "$PLAN_ID" \
    --key-id "$KEY_ID" \
    --key-type API_KEY

Step 5: Configure and invoke the endpoint from SQL Server 2025

With the Lambda function and API Gateway endpoint in place, the final step is to call them from SQL Server 2025. This involves storing the API key securely, creating a stored procedure that invokes the endpoint, and running tests to confirm the end-to-end flow. The following sections walk through each part, starting with credential setup.

5.1 Store the API key in a database scoped credential

We store the API key inside SQL Server rather than embedding it in the query text. A database master key encrypts the credential at rest, and the database scoped credential holds the API key as an HTTP header (x-api-key) that the sp_invoke_external_rest_endpoint procedure automatically attaches to each call. This keeps the key out of stored procedure definitions, query plans, and logs. The procedure references the credential by name and never sees the raw key.

Create the master key once per database. The IF NOT EXISTS check makes the script safe to re-run. Also, create a sample database named DemoDB using the script in the repository for this testing.

USE DemoDB;
GO
-- Create master key (one time per database)
IF NOT EXISTS (SELECT * FROM sys.symmetric_keys WHERE name = '##MS_DatabaseMasterKey##')
CREATE MASTER KEY ENCRYPTION BY PASSWORD = 'YourStr0ngP@ssword!'; -- Please change the password here
GO

Create the database scoped credential using the following command in SQL Server:

-- Create credential. NOTE the trailing slash in the name.
CREATE DATABASE SCOPED CREDENTIAL [https://<API_GATEWAY_BASE_URL>/]
WITH IDENTITY = 'HTTPEndpointHeaders',
SECRET = '{"x-api-key":"<YOUR_API_KEY>"}';
GO

Replace placeholders with your actual values:

<API_GATEWAY_BASE_URL> = Your API Gateway base URL WITH trailing slash
Example: https://abc123.execute-api.us-east-1.amazonaws.com/
<YOUR_API_KEY> = The API key value from Step 4.2

Two details matter for it to work:

Credential name = the endpoint’s base URL, with a trailing slash. SQL Server matches the credential to the call by treating the name as a path-boundary prefix of the request URL. Without the trailing slash, the match fails and you get a “cannot find the credential” error.

Identity = ‘HTTPEndpointHeaders’ tells SQL Server to send the credential’s secret as request headers (as opposed to a query string), which is how API Gateway expects the API key.

5.2 Create a procedure that mirrors the CLR function

This procedure is the drop-in replacement for the original CLR function we used in SQL Server 2016 to replicate the test. It has the same parameters (@csvData, @delimiter, @skipHeader), so existing callers don’t change. It builds a JSON request with FOR JSON, invokes the Lambda function through API Gateway with sp_invoke_external_rest_endpoint, and returns the parsed rows by using OPENJSON. For a line-by-line explanation, see How the procedure works in the repository.

CREATE OR ALTER PROCEDURE [dbo].[ParseCSV_Lambda]
    @csvData NVARCHAR(MAX),
    @delimiter NVARCHAR(10) = N',',
    @skipHeader BIT = 0
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @url NVARCHAR(4000) = N'API_GATEWAY_BASE_URL/prod/parse';
    /* Copy the URL from Step 3.8
    Example:-
    DECLARE @url NVARCHAR(4000) = N'https://txzgi4euy3.execute-api.us-east-1.amazonaws.com/prod/parse';
    */

    DECLARE @payload NVARCHAR(MAX);
    SET @payload = (
        SELECT @csvData AS csvData,
            @delimiter AS delimiter,
            CAST(@skipHeader AS BIT) AS skipHeader
        FROM (SELECT 1 AS x) d
        FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
    );

    -- Retry settings: retry only on transient failures with exponential backoff.
    DECLARE @raw_response NVARCHAR(MAX);
    DECLARE @maxAttempts INT = 4; -- 1 initial try + 3 retries
    DECLARE @attempt INT = 1;
    DECLARE @delaySec INT = 1; -- backoff seconds, doubles each retry (1s, 2s, 4s)
    DECLARE @delay CHAR(8); -- 'HH:MM:SS' string for WAITFOR DELAY (it rejects INT/TIME)
    DECLARE @httpCode INT;
    DECLARE @success NVARCHAR(10);

    WHILE @attempt <= @maxAttempts
    BEGIN
        BEGIN TRY
            EXEC sp_invoke_external_rest_endpoint
                @url = @url,
                @method = N'POST',
                @credential = [API_GATEWAY_BASE_URL],
                /* This should match to what you have used to create database scoped credential in step 5.1
                Example:-
                @credential = [https://txzgi4euy3.execute-api.us-east-1.amazonaws.com/],
                */
                @payload = @payload,
                @timeout = 60,
                @response = @raw_response OUTPUT;

            SET @httpCode = TRY_CAST(JSON_VALUE(@raw_response, '$.response.status.http.code') AS INT);
            SET @success = JSON_VALUE(@raw_response, '$.result.success');

            IF @httpCode = 200 AND (@success IS NULL OR @success <> 'false')
                BREAK; -- success

            IF (@httpCode = 429 OR @httpCode >= 500) AND @attempt < @maxAttempts
            BEGIN
                SET @delay = CONVERT(CHAR(8), DATEADD(SECOND, @delaySec, '00:00:00'), 108);
                WAITFOR DELAY @delay;
                SET @delaySec = @delaySec * 2;
                SET @attempt = @attempt + 1;
                CONTINUE;
            END;

            BREAK; -- non-transient error, or retries exhausted
        END TRY
        BEGIN CATCH
            IF @attempt >= @maxAttempts
                THROW; -- exhausted retries; surface the original error

            SET @delay = CONVERT(CHAR(8), DATEADD(SECOND, @delaySec, '00:00:00'), 108);
            WAITFOR DELAY @delay;
            SET @delaySec = @delaySec * 2;
            SET @attempt = @attempt + 1;
        END CATCH;
    END;

    IF @httpCode <> 200 OR @success = 'false'
    BEGIN
        DECLARE @err NVARCHAR(4000) = JSON_VALUE(@raw_response, '$.result.error');
        RAISERROR('CSV Parser Lambda error after %d attempt(s) (HTTP %d): %s', 16, 1, @attempt, @httpCode, @err);
        RETURN;
    END;

    DECLARE @rows NVARCHAR(MAX) = JSON_QUERY(@raw_response, '$.result.rows');

    SELECT
        JSON_VALUE(r.[value], '$.rowNum') AS RowNum,
        LTRIM(RTRIM(JSON_VALUE(r.[value], '$.fields[0]'))) AS Col1,
        JSON_VALUE(r.[value], '$.fields[1]') AS Col2,
        JSON_VALUE(r.[value], '$.fields[2]') AS Col3,
        JSON_VALUE(r.[value], '$.fields[3]') AS Col4,
        JSON_VALUE(r.[value], '$.fields[4]') AS Col5
    FROM OPENJSON(@rows) r;
END;

Consider using the ENCRYPTION option to create the stored procedure for enhanced security if required. Also, to mitigate the risk of unauthorized access or data transfer, follow the security best practices described in Ways to mitigate risk of unauthorized access or transfer of data.

5.3 Test with data the native parser can’t handle

DECLARE @csv NVARCHAR(MAX) = N'CustomerName,Address,City
"AnyCompany, Inc.","123 Any St, Suite 1
Building A","Anytown"';

EXEC dbo.ParseCSV_Lambda @csv, N',' , 1;

Returns one row. “AnyCompany, Inc.” stays a single field and the multi-line address is preserved, as shown in the following screenshot.

Query result showing the multi-line record parsed into a single row with AnyCompany, Inc. as one field

Figure 2: Query result showing the multi-line record parsed into a single row with three fields

Performance and cost

Unlike a CLR assembly, which runs in-process in microseconds, this pattern makes a network round-trip on every call. As a result, it suits batch or file-level operations, such as one call per file in an extract, transform, and load (ETL) run, rather than per-row calls inside a hot query. In our testing, parsing a 1,000-row multi-line file took about 235 ms end-to-end on a warm function. The SQL-side work is negligible (approximately 10 ms), so the round-trip dominates. The first invocation after an idle period may add roughly 1–3 seconds of Lambda cold-start latency. Subsequent calls in the same burst run warm. If you need predictable latency for time-sensitive jobs, enable Provisioned Concurrency on the function to keep it initialized. Round-trip latency depends on the network path between RDS and API Gateway. Keeping the Lambda and API Gateway in the same AWS Region as RDS minimizes it. You can estimate the execution time of Lambda separately from the network by looking at the function’s Amazon CloudWatch logs.

Tip: INSERT … EXEC dbo.ParseCSV_Lambda holds locks on the target table for the entire network round-trip (including any Lambda cold start). For loads into a permanent table, stage the parsed rows into a temp table first (INSERT INTO #Staging EXEC dbo.ParseCSV_Lambda …), then do a fast local INSERT from the temp table into the target. This keeps the round-trip latency out of the real table’s lock window. (Section C of script 05-tests.sql shows the temp-table staging pattern.)

For very large CSV files, split the payload into chunks before calling the procedure, such as one chunk per 1,000–5,000 rows. Both sp_invoke_external_rest_endpoint and Lambda’s synchronous invocation have a payload size limit (6 MB), so chunking avoids hitting that ceiling and keeps individual round-trip times predictable. As a rule of thumb, if a file is larger than 5–6 MB size, chunk it into smaller parts.

For batch workloads the cost is minimal. A typical nightly job of 1,000 file invocations per month costs well under $1/month across AWS Lambda and Amazon API Gateway combined (assuming approximately 128–512 MB memory, sub-second durations, and AWS Free Tier eligible request volumes). The dominant cost remains the Amazon RDS instance itself. Use the AWS Pricing Calculator to model your own request volume.

Clean up

To avoid ongoing charges, remove the resources you created. First, drop the objects in SQL Server by running the script 06-cleanup.sql in the repository. Delete the usage plan and API key. Then delete the clr-csv-parser Lambda function. Finally, delete any RDS instances, custom parameter groups, and Amazon Simple Storage Service (Amazon S3) test files that you created solely for this walkthrough.

Conclusion

In this post, we showed you how to replace an unsupported CLR assembly on Amazon RDS for SQL Server with an AWS Lambda function called natively from T-SQL through sp_invoke_external_rest_endpoint. With this pattern, you can retire CLR code while staying on fully managed Amazon RDS and reusing your existing .NET logic. Use this pattern for logic with no native equivalent in SQL Server. To get started, clone the sample code repository and run the walkthrough. For more detail, see Amazon RDS for SQL Server, AWS Lambda, and the parent post, Addressing CLR assembly deprecation in Amazon RDS for SQL Server.


About the authors

Raunak Gupta

Raunak Gupta

Raunak is a Sr. Database Solution Architect at AWS. He has been with AWS for more than 6 years and is also an Amazon Aurora and RDS subject matter expert. With over 17 years of professional experience, he provides technical assistance on database operational performance and database best practices to enterprise customers.

Dipin Sahadevan

Dipin Sahadevan

Dipin is a Sr. Cloud Support Engineer at AWS and a subject matter expert in Amazon RDS for SQL Server, Amazon RDS for PostgreSQL, and AWS Database Migration Service (AWS DMS). With 15 years of relational database experience, he helps enterprise customers troubleshoot RDS issues, guide migrations, optimize workloads, and prepare for critical events and production cutovers.