使用SAM部署API Gateway时遇CloudFormation模板Fn::ToJsonString不支持错误
问题现象
使用sam build/sam deploy部署API Gateway时触发如下错误:
Error: Failed to create changeset for the stack: my-api, ex: Waiter ChangeSetCreateComplete failed: Waiter encountered a terminal failure state: For expression "Status" we matched expected path: "FAILED" Status: FAILED. Reason: Template Error: Encountered unsupported function: Fn::ToJsonString Supported functions are: [Fn::Base64, Fn::GetAtt, Fn::GetAZs, Fn::ImportValue, Fn::Join, Fn::Split, Fn::FindInMap, Fn::Select, Ref, Fn::Equals, Fn::If, Fn::Not, Condition, Fn::And, Fn::Or, Fn::Contains, Fn::EachMemberEquals, Fn::EachMemberIn, Fn::ValueOf, Fn::ValueOfAll, Fn::RefAll, Fn::Sub, Fn::Cidr]
此前部署完全正常,仅修改过api.yaml,回滚该修改后问题仍存在。已尝试使用历史版本、调整Transform块中AWS::Serverless-2016-10-31与AWS::LanguageExtensions的顺序,但均未解决问题。
涉及的CloudFormation模板
AWSTemplateFormatVersion: '2010-09-09' Transform: - AWS::Serverless-2016-10-31 - AWS::LanguageExtensions Description: > Service root Parameters: EnvType: Description: 'Environment type' Default: test Type: String AllowedValues: ['prod', 'dev', 'stage', 'test'] DefaultServicesVersion: Default: latest Type: String ServicesPrefix: Type: String Default: '' DomainName: Type: String Default: api.link EdgeCertificateArn: Type: String Default: '' HostedZoneId: Type: String Default: '' WafRateLimit: Type: Number Default: '100' WafCountryCodes: Type: String Default: 'DE' JwtAudience: Type: String Default: 'dee' JwtIssuer: Type: String JwksUri: Type: String Conditions: IsEnvProd: !Equals [!Ref EnvType, 'prod'] IsEnvTest: !Equals [!Ref EnvType, 'test'] IsEnvNotTest: !Not [!Equals [!Ref EnvType, 'test']] Globals: Api: TracingEnabled: true OpenApiVersion: 3.0.1 Function: Timeout: 5 # Default timeout is 5 seconds Handler: lambda.handler Runtime: nodejs18.x Environment: Variables: AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 Resources: RestApi: Condition: IsEnvNotTest Type: AWS::Serverless::Api Properties: Name: !Sub '${AWS::StackName}-api' StageName: !Sub ${EnvType} AccessLogSetting: DestinationArn: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:DeeCloudWatchApiLogs:*' Format: Fn::ToJsonString: Fn::Transform: Name: AWS::Include Parameters: Location: './routes/access-log-settings.json' Tags: monitoring: SLO budgeting: SLO DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: './routes/api.yaml' Domain: CertificateArn: !Sub '${EdgeCertificateArn}' DomainName: !Sub '${DomainName}' EndpointConfiguration: EDGE Route53: HostedZoneId: !Sub '${HostedZoneId}' EvaluateTargetHealth: true RestApiAuthorizer: Condition: IsEnvNotTest Type: 'AWS::ApiGateway::Authorizer' Properties: AuthorizerCredentials: !GetAtt - LambdaAuthorizerInvocationRole - Arn AuthorizerResultTtlInSeconds: '3600' AuthorizerUri: !Join - '' - - 'arn:aws:apigateway:' - !Ref 'AWS::Region' - ':lambda:path/2015-03-31/functions/' - !GetAtt - RestApiAuthorizerFunction - Arn - /invocations Type: TOKEN IdentitySource: method.request.header.Authorization Name: !Sub ${AWS::StackName}-api-authorizer RestApiId: !Ref RestApi IdentityValidationExpression: ^Bearer [-0-9a-zA-z\.]*$ LambdaAuthorizerInvocationRole: Condition: IsEnvNotTest Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com - apigateway.amazonaws.com Action: - 'sts:AssumeRole' Policies: - PolicyName: !Sub ${AWS::StackName}-AWSLambdaRole PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - 'lambda:InvokeFunction' Resource: '*' RestApiAuthorizerFunction: Condition: IsEnvNotTest Type: AWS::Serverless::Function Properties: FunctionName: !Sub ${AWS::StackName}-api-authorizer-func CodeUri: auth/jwt-rsa-auth/src/ Environment: Variables: JWT_AUDIENCE: !Ref JwtAudience JWT_ISSUER: !Ref JwtIssuer JWKS_URI: !Ref JwksUri NODE_TLS_REJECT_UNAUTHORIZED: 0 #TODO: WARNING - DEV ONLY. Security Major issue. Remove later Role: !GetAtt - LambdaAuthorizerInvocationRole - Arn Tags: monitoring: SLO budgeting: SLO Policies: - AWSLambdaBasicExecutionRole - Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - apigateway.amazonaws.com Action: - 'lambda:InvokeFunction' Resource: !GetAtt RestApiAuthorizer.Arn TestingRestApi: Condition: IsEnvTest Type: AWS::Serverless::Api Properties: Name: !Sub '${AWS::StackName}-testing-api' StageName: !Sub ${EnvType} AccessLogSetting: DestinationArn: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:DeeCloudWatchApiLogs:*' Format: Fn::ToJsonString: Fn::Transform: Name: AWS::Include Parameters: Location: './routes/access-log-settings.json' Tags: monitoring: SLO budgeting: SLO DefinitionBody: Fn::Transform: Name: AWS::Include Parameters: Location: './routes/api.yaml' WafModule: Condition: IsEnvProd Type: CG::Security::RestApiWaf::MODULE Properties: RestApi: !Sub ${RestApi} RestApiStage: !Sub ${RestApi.Stage} WafRateLimit: !Sub ${WafRateLimit} WafCountryCodes: !Sub ${WafCountryCodes} ApiGatewayStepFunctionsRole: Type: AWS::IAM::Role Properties: Path: !Join ['', ['/', !Ref 'AWS::StackName', '/']] AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Sid: AllowApiGatewayServiceToAssumeRole Effect: Allow Action: - 'sts:AssumeRole' Principal: Service: - apigateway.amazonaws.com Policies: - PolicyName: CallStepFunctions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'states:StartExecution' Resource: '*' - PolicyName: DescribeStepFunctionExecutions PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 'states:DescribeExecution' Resource: '*' Outputs: ApiEndpoint: Description: API Endpoint Export: Name: !Sub ${AWS::StackName}-ApiEndpoint Value: Fn::If: - IsEnvTest - !Sub 'https://${TestingRestApi}.execute-api.${AWS::Region}.amazonaws.com/${TestingRestApi.Stage}' - !Sub 'https://${DomainName}'
解决方法
1. 调整Transform执行顺序
AWS::LanguageExtensions需要在AWS::Serverless-2016-10-31之前执行,因为它提供的Fn::ToJsonString函数需要先被CloudFormation识别,才能被后续的Serverless Transform使用。修改Transform块为:
Transform: - AWS::LanguageExtensions - AWS::Serverless-2016-10-31
2. 升级SAM CLI版本
旧版本的SAM CLI可能对AWS::LanguageExtensions的支持存在缺陷,执行以下命令升级到最新稳定版:
# pip安装的SAM pip install --upgrade aws-sam-cli # Homebrew安装的SAM brew upgrade aws-sam-cli
3. 替换嵌套的函数调用
如果调整顺序和升级版本仍无效,可以将Fn::ToJsonString与Fn::Transform的嵌套调用替换为直接构造JSON字符串的方式。例如将AccessLogSetting.Format部分修改为:
Format: !Sub | { "requestId": "$context.requestId", "ip": "$context.identity.sourceIp", "requestTime": "$context.requestTime", "httpMethod": "$context.httpMethod", "routeKey": "$context.routeKey", "status": "$context.status", "responseLength": "$context.responseLength" }
若需保留外部文件引用,可先通过AWS::Include引入JSON对象,再用Fn::Join或Fn::Sub手动转换为字符串(注意处理转义字符)。
4. 清理SAM缓存
删除本地的.aws-sam构建目录,避免缓存的旧模板干扰:
rm -rf .aws-sam
之后重新执行sam build && sam deploy。
5. 检查构建产物
执行sam build后,查看.aws-sam/build/template.yaml文件,确认Fn::ToJsonString是否被正确转换。若仍存在未解析的函数调用,说明SAM未正确处理LanguageExtensions,需进一步排查版本或配置问题。
内容的提问来源于stack exchange,提问作者HouseInTheForest

