1. Docs
  2. Infrastructure as Code
  3. Guides
  4. Migration
  5. Serverless Framework

Migrating from Serverless Framework to Pulumi

    If your team has already provisioned infrastructure using the Serverless Framework, and you’d like to adopt Pulumi, you have several strategies you can take:

    • Neo (Recommended): Since Serverless Framework deploys via CloudFormation, Neo can automatically convert your stacks and import existing resources with zero downtime.
    • Coexist with resources provisioned by the Serverless Framework by referencing CloudFormation stack outputs.
    • Import existing resources into Pulumi.
    • Rewrite your serverless.yml definitions as Pulumi code and incrementally migrate resources.

    How Serverless Framework uses CloudFormation

    Every time you run sls deploy, the Serverless Framework generates a CloudFormation template and deploys it as a stack. The stack is named using the pattern {service}-{stage}, for example, my-api-dev or my-api-prod. This stack contains all the resources defined in your serverless.yml, including:

    • Lambda functions and their execution roles
    • API Gateway REST APIs or HTTP APIs
    • Event source mappings (SQS, DynamoDB Streams, Kinesis)
    • CloudWatch log groups
    • Any resources you define in the resources: section (DynamoDB tables, SQS queues, S3 buckets, etc.)

    Because all Serverless Framework resources are CloudFormation resources, the CloudFormation migration strategies described in the CloudFormation migration guide apply directly.

    To find your CloudFormation stack names, run:

    # Using the Serverless Framework CLI
    sls info --stage dev
    
    # Or using the AWS CLI directly
    aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \
        --query "StackSummaries[?starts_with(StackName, 'my-api-')].StackName"
    

    Choosing a migration path

    Because the Serverless Framework creates standard CloudFormation stacks, Neo can convert them to Pulumi code automatically.

    1. Prerequisites:

    2. Identify your CloudFormation stacks: Find the stack names created by the Serverless Framework (e.g., my-api-dev, my-api-prod).

    3. Start the migration:

      "Convert my CloudFormation stack my-api-dev to Pulumi"
      
    4. Neo will:

      • Parse the CloudFormation stack and its resources
      • Generate equivalent Pulumi code for all resources (Lambda functions, API Gateway, IAM roles, etc.)
      • Import existing AWS resources without touching them
      • Verify zero changes with pulumi preview
    5. Review and commit:

      • Examine the generated Pulumi code
      • Confirm the preview shows no changes
      • Commit your new Pulumi program

    For a detailed walkthrough, see the Neo migration blog post.

    When to use manual migration instead

    While Neo handles most CloudFormation stacks automatically, you might need manual migration for:

    • Custom CloudFormation resources or macros not yet supported by Neo
    • Scenarios where you want to fundamentally restructure your infrastructure during migration
    • Cases where you want to adopt resources incrementally across multiple Serverless Framework services

    If you want to restructure your infrastructure, we recommend completing the migration first and then refactoring your Pulumi code.

    Alternative migration paths

    If Neo doesn’t support your specific use case, or if you prefer manual control over the migration process, the options below provide flexibility to coexist with or migrate from the Serverless Framework at your own pace.

    Referencing stack outputs

    You can reference existing Serverless Framework stacks from your Pulumi program without taking over management of their resources. This is useful when you want to build new infrastructure that depends on resources already managed by the Serverless Framework.

    The Serverless Framework exports several outputs from each CloudFormation stack, including ServiceEndpoint (the API Gateway URL) and {FunctionName}LambdaFunctionQualifiedArn for each function.

    The following example reads a Serverless Framework stack named my-api-dev and uses its API endpoint and a function ARN:

    import * as aws from "@pulumi/aws";
    
    const serverlessStack = aws.cloudformation.getStackOutput({
        name: "my-api-dev",
    });
    
    const apiEndpoint = serverlessStack.outputs["ServiceEndpoint"];
    const processOrderArn = serverlessStack.outputs["ProcessOrderLambdaFunctionQualifiedArn"];
    
    // Use these values in new infrastructure
    const queue = new aws.sqs.Queue("new-queue");
    
    export const endpoint = apiEndpoint;
    export const orderFunctionArn = processOrderArn;
    
    import pulumi
    import pulumi_aws as aws
    
    serverless_stack = aws.cloudformation.get_stack(
        name="my-api-dev"
    )
    
    api_endpoint = serverless_stack.outputs["ServiceEndpoint"]
    process_order_arn = serverless_stack.outputs["ProcessOrderLambdaFunctionQualifiedArn"]
    
    # Use these values in new infrastructure
    queue = aws.sqs.Queue("new-queue")
    
    pulumi.export("endpoint", api_endpoint)
    pulumi.export("order_function_arn", process_order_arn)
    
    package main
    
    import (
    	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/cloudformation"
    	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/sqs"
    	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
    	pulumi.Run(func(ctx *pulumi.Context) error {
    		serverlessStack := cloudformation.LookupStackOutput(ctx, cloudformation.LookupStackOutputArgs{
    			Name: pulumi.String("my-api-dev"),
    		})
    
    		apiEndpoint := serverlessStack.Outputs().MapIndex(pulumi.String("ServiceEndpoint"))
    		processOrderArn := serverlessStack.Outputs().MapIndex(pulumi.String("ProcessOrderLambdaFunctionQualifiedArn"))
    
    		_, err := sqs.NewQueue(ctx, "new-queue", nil)
    		if err != nil {
    			return err
    		}
    
    		ctx.Export("endpoint", apiEndpoint)
    		ctx.Export("orderFunctionArn", processOrderArn)
    		return nil
    	})
    }
    
    using System.Collections.Generic;
    
    using Pulumi;
    using CloudFormation = Pulumi.Aws.CloudFormation;
    using Sqs = Pulumi.Aws.Sqs;
    
    return await Deployment.RunAsync(async () =>
    {
        var serverlessStack = await CloudFormation.GetStack.InvokeAsync(
            new CloudFormation.GetStackArgs
            {
                Name = "my-api-dev",
            }
        );
    
        var apiEndpoint = serverlessStack.Outputs["ServiceEndpoint"];
        var processOrderArn = serverlessStack.Outputs["ProcessOrderLambdaFunctionQualifiedArn"];
    
        var queue = new Sqs.Queue("new-queue");
    
        return new Dictionary<string, object?>
        {
            { "endpoint", apiEndpoint },
            { "orderFunctionArn", processOrderArn },
        };
    });
    

    Run pulumi up and the Pulumi runtime queries the CloudFormation stack and retrieves its output values. The Serverless Framework stack is treated as read-only, and Pulumi will not attempt to modify it or any resources managed by it.

    Resource mapping

    The following table maps common serverless.yml configuration to the equivalent Pulumi AWS resources. Use this as a reference when rewriting your Serverless Framework definitions as Pulumi code.

    Serverless Framework (serverless.yml)Pulumi AWS resource
    functions.[name]aws.lambda.Function
    functions.[name].events[].httpApiaws.apigatewayv2.Api
    functions.[name].events[].httpaws.apigateway.RestApi + related resources
    functions.[name].events[].sqsaws.sqs.Queue + aws.lambda.EventSourceMapping
    functions.[name].events[].snsaws.sns.Topic + aws.sns.TopicSubscription
    functions.[name].events[].s3aws.s3.BucketNotification
    functions.[name].events[].scheduleaws.cloudwatch.EventRule + aws.cloudwatch.EventTarget
    functions.[name].events[].eventBridgeaws.cloudwatch.EventRule + aws.cloudwatch.EventTarget
    provider.iam.role.statementsaws.iam.Role + aws.iam.RolePolicy
    provider.environmentenvironment argument on aws.lambda.Function
    resources.Resources (DynamoDB)aws.dynamodb.Table
    resources.Resources (S3)aws.s3.BucketV2
    resources.Resources (SES)aws.ses.DomainIdentity, aws.ses.EmailIdentity
    Stages (--stage dev)Pulumi stacks (pulumi stack select dev)

    Migration example

    The following example shows a typical serverless.yml excerpt and its equivalent Pulumi program. This example defines a Lambda function with an HTTP API endpoint and a DynamoDB table.

    Serverless Framework (serverless.yml)

    service: my-api
    
    provider:
      name: aws
      runtime: nodejs20.x
      stage: dev
      environment:
        ORDERS_TABLE: !Ref OrdersTable
    
    functions:
      createOrder:
        handler: src/handlers/createOrder.handler
        events:
          - httpApi:
              path: /orders
              method: post
    
    resources:
      Resources:
        OrdersTable:
          Type: AWS::DynamoDB::Table
          Properties:
            TableName: ${self:service}-orders-${self:provider.stage}
            BillingMode: PAY_PER_REQUEST
            AttributeDefinitions:
              - AttributeName: id
                AttributeType: S
            KeySchema:
              - AttributeName: id
                KeyType: HASH
    

    Pulumi equivalent

    import * as pulumi from "@pulumi/pulumi";
    import * as aws from "@pulumi/aws";
    
    const stage = pulumi.getStack();
    
    // DynamoDB table
    const ordersTable = new aws.dynamodb.Table("orders-table", {
        name: `my-api-orders-${stage}`,
        billingMode: "PAY_PER_REQUEST",
        hashKey: "id",
        attributes: [
            { name: "id", type: "S" },
        ],
    });
    
    // IAM role for the Lambda function
    const lambdaRole = new aws.iam.Role("create-order-role", {
        assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({
            Service: "lambda.amazonaws.com",
        }),
        managedPolicyArns: [
            aws.iam.ManagedPolicy.AWSLambdaBasicExecutionRole,
        ],
    });
    
    const lambdaPolicy = new aws.iam.RolePolicy("create-order-policy", {
        role: lambdaRole.id,
        policy: ordersTable.arn.apply(arn => JSON.stringify({
            Version: "2012-10-17",
            Statement: [{
                Effect: "Allow",
                Action: [
                    "dynamodb:PutItem",
                    "dynamodb:GetItem",
                    "dynamodb:Query",
                ],
                Resource: arn,
            }],
        })),
    });
    
    // Lambda function
    const createOrderFn = new aws.lambda.Function("create-order", {
        runtime: aws.lambda.Runtime.NodeJS20dX,
        handler: "src/handlers/createOrder.handler",
        role: lambdaRole.arn,
        code: new pulumi.asset.FileArchive("./app"),
        environment: {
            variables: {
                ORDERS_TABLE: ordersTable.name,
            },
        },
    });
    
    // HTTP API (API Gateway v2)
    const api = new aws.apigatewayv2.Api("api", {
        protocolType: "HTTP",
    });
    
    const integration = new aws.apigatewayv2.Integration("create-order-integration", {
        apiId: api.id,
        integrationType: "AWS_PROXY",
        integrationUri: createOrderFn.arn,
        payloadFormatVersion: "2.0",
    });
    
    const route = new aws.apigatewayv2.Route("create-order-route", {
        apiId: api.id,
        routeKey: "POST /orders",
        target: pulumi.interpolate`integrations/${integration.id}`,
    });
    
    const apiStage = new aws.apigatewayv2.Stage("api-stage", {
        apiId: api.id,
        name: "$default",
        autoDeploy: true,
    });
    
    const lambdaPermission = new aws.lambda.Permission("api-lambda-permission", {
        action: "lambda:InvokeFunction",
        function: createOrderFn.name,
        principal: "apigateway.amazonaws.com",
        sourceArn: pulumi.interpolate`${api.executionArn}/*/*`,
    });
    
    export const endpoint = api.apiEndpoint;
    export const tableName = ordersTable.name;
    
    import json
    
    import pulumi
    import pulumi_aws as aws
    
    stage = pulumi.get_stack()
    
    # DynamoDB table
    orders_table = aws.dynamodb.Table("orders-table",
        name=f"my-api-orders-{stage}",
        billing_mode="PAY_PER_REQUEST",
        hash_key="id",
        attributes=[
            aws.dynamodb.TableAttributeArgs(name="id", type="S"),
        ],
    )
    
    # IAM role for the Lambda function
    lambda_role = aws.iam.Role("create-order-role",
        assume_role_policy=json.dumps({
            "Version": "2012-10-17",
            "Statement": [{
                "Action": "sts:AssumeRole",
                "Effect": "Allow",
                "Principal": {"Service": "lambda.amazonaws.com"},
            }],
        }),
        managed_policy_arns=[
            aws.iam.ManagedPolicy.AWS_LAMBDA_BASIC_EXECUTION_ROLE,
        ],
    )
    
    lambda_policy = aws.iam.RolePolicy("create-order-policy",
        role=lambda_role.id,
        policy=orders_table.arn.apply(lambda arn: json.dumps({
            "Version": "2012-10-17",
            "Statement": [{
                "Effect": "Allow",
                "Action": [
                    "dynamodb:PutItem",
                    "dynamodb:GetItem",
                    "dynamodb:Query",
                ],
                "Resource": arn,
            }],
        })),
    )
    
    # Lambda function
    create_order_fn = aws.lambda_.Function("create-order",
        runtime=aws.lambda_.Runtime.NODE_JS20D_X,
        handler="src/handlers/createOrder.handler",
        role=lambda_role.arn,
        code=pulumi.FileArchive("./app"),
        environment=aws.lambda_.FunctionEnvironmentArgs(
            variables={"ORDERS_TABLE": orders_table.name},
        ),
    )
    
    # HTTP API (API Gateway v2)
    api = aws.apigatewayv2.Api("api",
        protocol_type="HTTP",
    )
    
    integration = aws.apigatewayv2.Integration("create-order-integration",
        api_id=api.id,
        integration_type="AWS_PROXY",
        integration_uri=create_order_fn.arn,
        payload_format_version="2.0",
    )
    
    route = aws.apigatewayv2.Route("create-order-route",
        api_id=api.id,
        route_key="POST /orders",
        target=integration.id.apply(lambda id: f"integrations/{id}"),
    )
    
    api_stage = aws.apigatewayv2.Stage("api-stage",
        api_id=api.id,
        name="$default",
        auto_deploy=True,
    )
    
    lambda_permission = aws.lambda_.Permission("api-lambda-permission",
        action="lambda:InvokeFunction",
        function=create_order_fn.name,
        principal="apigateway.amazonaws.com",
        source_arn=api.execution_arn.apply(lambda arn: f"{arn}/*/*"),
    )
    
    pulumi.export("endpoint", api.api_endpoint)
    pulumi.export("table_name", orders_table.name)
    
    package main
    
    import (
    	"encoding/json"
    	"fmt"
    
    	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/apigatewayv2"
    	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/dynamodb"
    	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/iam"
    	"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/lambda"
    	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
    	pulumi.Run(func(ctx *pulumi.Context) error {
    		stage := ctx.Stack()
    
    		// DynamoDB table
    		ordersTable, err := dynamodb.NewTable(ctx, "orders-table", &dynamodb.TableArgs{
    			Name:        pulumi.Sprintf("my-api-orders-%s", stage),
    			BillingMode: pulumi.String("PAY_PER_REQUEST"),
    			HashKey:     pulumi.String("id"),
    			Attributes: dynamodb.TableAttributeArray{
    				&dynamodb.TableAttributeArgs{
    					Name: pulumi.String("id"),
    					Type: pulumi.String("S"),
    				},
    			},
    		})
    		if err != nil {
    			return err
    		}
    
    		// IAM role for the Lambda function
    		assumeRolePolicy, _ := json.Marshal(map[string]interface{}{
    			"Version": "2012-10-17",
    			"Statement": []map[string]interface{}{
    				{
    					"Action":    "sts:AssumeRole",
    					"Effect":    "Allow",
    					"Principal": map[string]string{"Service": "lambda.amazonaws.com"},
    				},
    			},
    		})
    
    		lambdaRole, err := iam.NewRole(ctx, "create-order-role", &iam.RoleArgs{
    			AssumeRolePolicy: pulumi.String(string(assumeRolePolicy)),
    			ManagedPolicyArns: pulumi.StringArray{
    				iam.ManagedPolicyAWSLambdaBasicExecutionRole,
    			},
    		})
    		if err != nil {
    			return err
    		}
    
    		_, err = iam.NewRolePolicy(ctx, "create-order-policy", &iam.RolePolicyArgs{
    			Role: lambdaRole.ID(),
    			Policy: ordersTable.Arn.ApplyT(func(arn string) (string, error) {
    				policy, _ := json.Marshal(map[string]interface{}{
    					"Version": "2012-10-17",
    					"Statement": []map[string]interface{}{
    						{
    							"Effect":   "Allow",
    							"Action":   []string{"dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:Query"},
    							"Resource": arn,
    						},
    					},
    				})
    				return string(policy), nil
    			}).(pulumi.StringOutput),
    		})
    		if err != nil {
    			return err
    		}
    
    		// Lambda function
    		createOrderFn, err := lambda.NewFunction(ctx, "create-order", &lambda.FunctionArgs{
    			Runtime: pulumi.String("nodejs20.x"),
    			Handler: pulumi.String("src/handlers/createOrder.handler"),
    			Role:    lambdaRole.Arn,
    			Code:    pulumi.NewFileArchive("./app"),
    			Environment: &lambda.FunctionEnvironmentArgs{
    				Variables: pulumi.StringMap{
    					"ORDERS_TABLE": ordersTable.Name,
    				},
    			},
    		})
    		if err != nil {
    			return err
    		}
    
    		// HTTP API (API Gateway v2)
    		api, err := apigatewayv2.NewApi(ctx, "api", &apigatewayv2.ApiArgs{
    			ProtocolType: pulumi.String("HTTP"),
    		})
    		if err != nil {
    			return err
    		}
    
    		integration, err := apigatewayv2.NewIntegration(ctx, "create-order-integration", &apigatewayv2.IntegrationArgs{
    			ApiId:                api.ID(),
    			IntegrationType:      pulumi.String("AWS_PROXY"),
    			IntegrationUri:       createOrderFn.Arn,
    			PayloadFormatVersion: pulumi.String("2.0"),
    		})
    		if err != nil {
    			return err
    		}
    
    		_, err = apigatewayv2.NewRoute(ctx, "create-order-route", &apigatewayv2.RouteArgs{
    			ApiId:    api.ID(),
    			RouteKey: pulumi.String("POST /orders"),
    			Target:   pulumi.Sprintf("integrations/%s", integration.ID()),
    		})
    		if err != nil {
    			return err
    		}
    
    		_, err = apigatewayv2.NewStage(ctx, "api-stage", &apigatewayv2.StageArgs{
    			ApiId:      api.ID(),
    			Name:       pulumi.String("$default"),
    			AutoDeploy: pulumi.Bool(true),
    		})
    		if err != nil {
    			return err
    		}
    
    		_, err = lambda.NewPermission(ctx, "api-lambda-permission", &lambda.PermissionArgs{
    			Action:    pulumi.String("lambda:InvokeFunction"),
    			Function:  createOrderFn.Name,
    			Principal: pulumi.String("apigateway.amazonaws.com"),
    			SourceArn: api.ExecutionArn.ApplyT(func(arn string) string {
    				return fmt.Sprintf("%s/*/*", arn)
    			}).(pulumi.StringOutput),
    		})
    		if err != nil {
    			return err
    		}
    
    		ctx.Export("endpoint", api.ApiEndpoint)
    		ctx.Export("tableName", ordersTable.Name)
    		return nil
    	})
    }
    
    using System.Collections.Generic;
    using System.Text.Json;
    
    using Pulumi;
    using Aws = Pulumi.Aws;
    
    return await Deployment.RunAsync(() =>
    {
        var stage = Deployment.Instance.StackName;
    
        // DynamoDB table
        var ordersTable = new Aws.DynamoDB.Table("orders-table", new()
        {
            Name = $"my-api-orders-{stage}",
            BillingMode = "PAY_PER_REQUEST",
            HashKey = "id",
            Attributes = new[]
            {
                new Aws.DynamoDB.Inputs.TableAttributeArgs
                {
                    Name = "id",
                    Type = "S",
                },
            },
        });
    
        // IAM role for the Lambda function
        var lambdaRole = new Aws.Iam.Role("create-order-role", new()
        {
            AssumeRolePolicy = JsonSerializer.Serialize(new
            {
                Version = "2012-10-17",
                Statement = new[]
                {
                    new
                    {
                        Action = "sts:AssumeRole",
                        Effect = "Allow",
                        Principal = new { Service = "lambda.amazonaws.com" },
                    },
                },
            }),
            ManagedPolicyArns = new[]
            {
                "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
            },
        });
    
        var lambdaPolicy = new Aws.Iam.RolePolicy("create-order-policy", new()
        {
            Role = lambdaRole.Id,
            Policy = ordersTable.Arn.Apply(arn => JsonSerializer.Serialize(new
            {
                Version = "2012-10-17",
                Statement = new[]
                {
                    new
                    {
                        Effect = "Allow",
                        Action = new[] { "dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:Query" },
                        Resource = arn,
                    },
                },
            })),
        });
    
        // Lambda function
        var createOrderFn = new Aws.Lambda.Function("create-order", new()
        {
            Runtime = Aws.Lambda.Runtime.NodeJS20dX,
            Handler = "src/handlers/createOrder.handler",
            Role = lambdaRole.Arn,
            Code = new FileArchive("./app"),
            Environment = new Aws.Lambda.Inputs.FunctionEnvironmentArgs
            {
                Variables = { { "ORDERS_TABLE", ordersTable.Name } },
            },
        });
    
        // HTTP API (API Gateway v2)
        var api = new Aws.ApiGatewayV2.Api("api", new()
        {
            ProtocolType = "HTTP",
        });
    
        var integration = new Aws.ApiGatewayV2.Integration("create-order-integration", new()
        {
            ApiId = api.Id,
            IntegrationType = "AWS_PROXY",
            IntegrationUri = createOrderFn.Arn,
            PayloadFormatVersion = "2.0",
        });
    
        var route = new Aws.ApiGatewayV2.Route("create-order-route", new()
        {
            ApiId = api.Id,
            RouteKey = "POST /orders",
            Target = integration.Id.Apply(id => $"integrations/{id}"),
        });
    
        var apiStage = new Aws.ApiGatewayV2.Stage("api-stage", new()
        {
            ApiId = api.Id,
            Name = "$default",
            AutoDeploy = true,
        });
    
        var lambdaPermission = new Aws.Lambda.Permission("api-lambda-permission", new()
        {
            Action = "lambda:InvokeFunction",
            Function = createOrderFn.Name,
            Principal = "apigateway.amazonaws.com",
            SourceArn = api.ExecutionArn.Apply(arn => $"{arn}/*/*"),
        });
    
        return new Dictionary<string, object?>
        {
            { "endpoint", api.ApiEndpoint },
            { "tableName", ordersTable.Name },
        };
    });
    

    With Pulumi, you get the full power of a programming language. You can create reusable functions, use loops to create multiple similar resources, add conditional logic, and write tests for your infrastructure code.

    Importing existing resources

    If you have existing resources created by the Serverless Framework that you want to bring under Pulumi’s management without recreating them, use Pulumi’s import feature.

    Step 1: Identify your resources

    List the resources in your Serverless Framework CloudFormation stack:

    aws cloudformation list-stack-resources --stack-name my-api-dev \
        --query "StackResourceSummaries[].{Type:ResourceType,LogicalId:LogicalResourceId,PhysicalId:PhysicalResourceId}" \
        --output table
    

    Step 2: Import resources into Pulumi

    You can import resources using the pulumi import CLI command or the import resource option in code.

    Using the CLI:

    # Import a Lambda function
    pulumi import aws:lambda/function:Function create-order my-api-dev-createOrder
    
    # Import a DynamoDB table
    pulumi import aws:dynamodb/table:Table orders-table my-api-orders-dev
    
    # Import an API Gateway v2 API
    pulumi import aws:apigatewayv2/api:Api api abc123def
    

    Using the import option in code (TypeScript example):

    const ordersTable = new aws.dynamodb.Table("orders-table", {
        name: "my-api-orders-dev",
        billingMode: "PAY_PER_REQUEST",
        hashKey: "id",
        attributes: [
            { name: "id", type: "S" },
        ],
    }, { import: "my-api-orders-dev" });
    

    After the import completes and pulumi preview shows no changes, remove the import option from your code. Pulumi now manages the resource.

    Step 3: Remove resources from CloudFormation

    After importing your resources into Pulumi, remove them from CloudFormation management to avoid dual management. This step is not a prerequisite for import — Pulumi import works at the AWS resource level regardless of CloudFormation — but you should complete it to prevent conflicting updates.

    Before deleting the CloudFormation stack, set a DeletionPolicy of Retain on every resource so that AWS preserves them when the stack is removed.

    User-defined resources

    For resources you defined in the resources.Resources section of serverless.yml, add the policy directly:

    resources:
      Resources:
        OrdersTable:
          Type: AWS::DynamoDB::Table
          DeletionPolicy: Retain
          Properties:
            # ... existing properties
    

    Deploy the change with sls deploy to apply the retention policy.

    Auto-generated resources

    The Serverless Framework auto-generates resources (Lambda functions, IAM roles, API Gateway, CloudWatch log groups) that cannot have DeletionPolicy set via serverless.yml. To retain these resources, update the compiled CloudFormation template directly:

    1. Generate the template:

      sls package --stage dev
      
    2. Add DeletionPolicy: Retain to every resource in the compiled template using jq:

      jq '.Resources |= with_entries(.value += {"DeletionPolicy": "Retain"})' \
          .serverless/cloudformation-template-update-stack.json > retained-template.json
      
    3. Update the CloudFormation stack with the modified template:

      aws cloudformation update-stack \
          --stack-name my-api-dev \
          --template-body file://retained-template.json \
          --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM
      
    4. Wait for the update to complete, then remove the stack:

      aws cloudformation wait stack-update-complete --stack-name my-api-dev
      sls remove --stage dev
      

    All resources will remain in AWS, now managed entirely by Pulumi.

    For detailed AWS import ID formats and troubleshooting, see AWS import IDs and special cases.

    Managing stages with Pulumi stacks

    The Serverless Framework uses --stage to deploy the same service to multiple environments. In Pulumi, this maps directly to stacks:

    # Create stacks for each stage
    pulumi stack init dev
    pulumi stack init staging
    pulumi stack init prod
    
    # Set stage-specific configuration
    pulumi config set --stack dev aws:region us-east-1
    pulumi config set --stack prod aws:region us-west-2
    

    In your Pulumi program, use pulumi.getStack() (or equivalent) to get the current stack name, which replaces the Serverless Framework ${self:provider.stage} variable.

    Stage-specific variables from serverless.yml’s custom: section become Pulumi config values:

    # Instead of serverless.yml custom variables:
    # custom:
    #   tableName:
    #     dev: my-table-dev
    #     prod: my-table-prod
    
    pulumi config set --stack dev tableName my-table-dev
    pulumi config set --stack prod tableName my-table-prod
    
    const config = new pulumi.Config();
    const tableName = config.require("tableName");
    

    For more advanced environment management, consider Pulumi ESC (Environments, Secrets, and Configuration) which provides hierarchical configuration, secrets management, and environment composition.

    Key differences and benefits

    Migrating from the Serverless Framework to Pulumi gives you:

    • Real programming languages instead of YAML configuration, with IDE support, type checking, and testing
    • Full cloud coverage: manage Lambda functions alongside databases, queues, VPCs, DNS, CDNs, and any other cloud resource, all in one program
    • Built-in state management: no separate CloudFormation dependency or stack limits
    • Policy as code: enforce compliance and security rules across all your infrastructure
    • Multi-cloud support: use the same tool and workflows across AWS, Azure, Google Cloud, and 100+ providers
    • Pulumi ESC: centralized secrets and configuration management that replaces scattered environment variable management

    To learn more about how Pulumi compares to the Serverless Framework, see the Pulumi vs. Serverless Framework comparison.