8

问题

首次使用阶段名称创建 API Gateway 部署,并创建阶段以配置 X-RAY 或 CloudWatch 日志记录时,将导致“阶段已存在”。

resource "aws_api_gateway_deployment" "this" {
  rest_api_id = aws_api_gateway_rest_api.mysfit.id
  stage_name  = "${var.ENV}"
  variables = {
    deployed_at = timestamp()
  }
  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_api_gateway_stage" "this" {
  stage_name    = var.ENV
  rest_api_id   = aws_api_gateway_rest_api.mysfit.id
  deployment_id = aws_api_gateway_deployment.this.id

  dynamic "access_log_settings" {
    for_each = var.enable_apigw_stage_cloudwatch_access_log ? [1] : []
    content {
      destination_arn = module.cloudwatch.cloudwatch_loggroup_arn
      format          = file("${path.module}/apigw_access_log_format.json")
    }
  }

  xray_tracing_enabled = var.xray_tracing_enabled

  tags = {
    Project     = var.PROJECT
    Environment = var.ENV
  }
}

解决方法是省略 aws_api_gateway_deployment 中的 stage_name,因为 stage 对于 API Gateway 部署是可选的。但是,部署的 invoke_url 在 URL 路径中没有阶段部分。

这是特定于 Terraform 的问题还是 API 网关问题?

参考

4

2 回答 2

9

据我了解,API Gateway 部署设计存在逻辑缺陷。

API 部署是部署到一个阶段,如在 Amazon API Gateway 中部署 REST API

要部署 API,您需要创建 API 部署并将其与阶段相关联。阶段是对 API 生命周期状态的逻辑引用(例如 dev、prod、beta、v2)。

但是,创建阶段 API 需要现有部署。我相信,部署和阶段之间的这种循环依赖是问题的根源。

问题 1

create-stage API 需要使用 --deployment-id 参数进行部署。因此,我们需要先创建一个 API 部署。

这是第一个问题。如果我们为创建部署指定一个阶段,它会创建该阶段。那我们就不能自己创造舞台。

当我们使用 CloudFormation 或 Terraform 等配置管理工具时,这会导致“Stage 已存在”异常,因为我们将尝试自己创建阶段资源。

因此,我们无法在首次创建 API 部署时指定阶段。

问题 2

由于问题 1,我们要管理阶段资源的创建,我们需要首先创建一个虚拟部署,以便我们可以使用虚拟创建一个阶段。这一步浪费部署创建是第二个问题。虽然阶段指向部署,但部署并不能完全识别阶段,因为如果尝试从部署中获取调用 URL,它不包括阶段。

创建阶段后,最后我们可以创建另一个指定阶段的 API 部署。由于该阶段已经存在,部署将引用该阶段,并且调用 URL 将包含该阶段。

例子

地形

#--------------------------------------------------------------------------------
# Dummy API Deployment
#--------------------------------------------------------------------------------
resource "aws_api_gateway_deployment" "dummy" {
  rest_api_id = "${aws_api_gateway_rest_api.this.id}"

  #--------------------------------------------------------------------------------
  # To avoid State already exists
  # https://github.com/terraform-providers/terraform-provider-aws/issues/2918
  #--------------------------------------------------------------------------------
  #stage_name  = "${var.ENV}"

  #--------------------------------------------------------------------------------
  # Force re-deployment at each run. Alternative is to verify MD5 of API GW files.
  #--------------------------------------------------------------------------------
  # https://medium.com/coryodaniel/til-forcing-terraform-to-deploy-a-aws-api-gateway-deployment-ed36a9f60c1a
  # https://github.com/hashicorp/terraform/issues/6613
  # Terraform’s aws_api_gateway_deployment won’t deploy subsequent releases in the event
  # that something has changed in an integration, method, etc
  #--------------------------------------------------------------------------------
  stage_description = "Deployment at ${timestamp()}"

  lifecycle {
    create_before_destroy = true
  }

  depends_on = [
    #--------------------------------------------------------------------------------
    # [aws_api_gateway_account.this]
    # To avoid the error: Updating API Gateway Stage failed:
    # BadRequestException: CloudWatch Logs role ARN must be set in account settings to enable logging.
    #--------------------------------------------------------------------------------
    "aws_api_gateway_account.this",

    #--------------------------------------------------------------------------------
    # To avoid NotFoundException: Invalid Integration identifier specified
    #--------------------------------------------------------------------------------
    "aws_api_gateway_integration.ping_put",
  ]
  #--------------------------------------------------------------------------------
}

#--------------------------------------------------------------------------------
# Create a stage refering to the dummy.
# The 2nd/true deployment will later refer to this stage
#--------------------------------------------------------------------------------
resource "aws_api_gateway_stage" "this" {
  stage_name    = var.ENV
  rest_api_id   = aws_api_gateway_rest_api.this.id
  deployment_id = aws_api_gateway_deployment.dummy.id

  xray_tracing_enabled = var.apigw_xray_tracing_enabled

  tags = {
    Project     = var.PROJECT
    Environment = var.ENV
  }

  depends_on = [
    aws_api_gateway_deployment.dummy
  ]
}

#--------------------------------------------------------------------------------
# Legitimate API Deployment
#--------------------------------------------------------------------------------
resource "aws_api_gateway_deployment" "this" {
  rest_api_id = aws_api_gateway_rest_api.this.id
  stage_name  = aws_api_gateway_stage.this.stage_name

  lifecycle {
    create_before_destroy = true
  }
}

云形成

AWSTemplateFormatVersion: "2010-09-09"
Description: "My API Gateway and Lambda function"

Parameters:
  apiGatewayStageName:
    Type: "String"
    Default: "devStage"

  lambdaFunctionName:
    Type: "String"
    Default: "my-lambda-function"

Resources:
  apiGateway:
    Type: "AWS::ApiGateway::RestApi"
    Properties:
      Name: "test-api"
      Description: "My Test API"

  apiGatewayRootMethod:
    Type: "AWS::ApiGateway::Method"
    Properties:
      AuthorizationType: "NONE"
      HttpMethod: "GET"
      Integration:
        IntegrationHttpMethod: "POST"
        Type: "AWS_PROXY"
        Uri: !Sub
          - "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${lambdaArn}/invocations"
          - lambdaArn: !GetAtt "lambdaFunction.Arn"
      ResourceId: !GetAtt "apiGateway.RootResourceId"
      RestApiId: !Ref "apiGateway"

  apiGatewayDeployment:
    Type: "AWS::ApiGateway::Deployment"
    DependsOn:
      - "apiGatewayRootMethod"
    Properties:
      RestApiId: !Ref "apiGateway"
      StageName: ""

  apiGatewayStage:
    Type: "AWS::ApiGateway::Stage"
    Properties:
      StageName: !Ref "apiGatewayStageName"
      RestApiId: !Ref "apiGateway"
      DeploymentId: !Ref "apiGatewayDeployment"
      MethodSettings:
        - ResourcePath: /
          HttpMethod: "GET"
          MetricsEnabled: 'true'
          DataTraceEnabled: 'true'

  lambdaFunction:
    Type: "AWS::Lambda::Function"
    Properties:
      Code:
        ZipFile: |
          def handler(event,context):
            return {
              'body': 'Hello World from Lambda',
              'headers': {
                'Content-Type': 'text/plain'
              },
              'statusCode': 200
            }
      Description: "My function"
      FunctionName: !Ref "lambdaFunctionName"
      Handler: "index.handler"
      MemorySize: 256
      Role: !GetAtt "lambdaIAMRole.Arn"
      Runtime: "python3.7"
      Timeout: 30

  lambdaApiGatewayInvoke:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt "lambdaFunction.Arn"
      Principal: "apigateway.amazonaws.com"
      SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${apiGateway}/*/GET/"

  lambdaIAMRole:
    Type: "AWS::IAM::Role"
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Action:
              - "sts:AssumeRole"
            Effect: "Allow"
            Principal:
              Service:
                - "lambda.amazonaws.com"
      Policies:
        - PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Effect: "Allow"
                Resource:
                  - !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${lambdaFunctionName}:*"
          PolicyName: "lambda"

  lambdaLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/aws/lambda/${lambdaFunctionName}"
      RetentionInDays: 90

Outputs:
  apiGatewayInvokeURL:
    Value: !Sub "https://${apiGateway}.execute-api.${AWS::Region}.amazonaws.com/${apiGatewayStageName}"

  lambdaArn:
    Value: !GetAtt "lambdaFunction.Arn"
于 2020-04-09T02:27:23.747 回答
1

引用文档

AWS::ApiGateway::Deployment 资源将 API Gateway RestApi 资源部署到stage

这意味着,如果您正在创建一个 AWS::ApiGateway::Stage 资源,其阶段名称与您传递给 AWS::ApiGateway::Deployment 的阶段名称相同,那么 Cloudformation 脚本将尝试创建两个具有相同名称的阶段 - 因此提示错误。

因此,解决方案是将notStage name 传递到 Deployment 资源中。这将起作用:

Parameters:
  ENVIRONMENT:
    Type: String

Resources:
  ContactsStage:
    Type: AWS::ApiGateway::Stage
    Properties:
      StageName: !Ref ENVIRONMENT
      RestApiId: !Ref MyApiGateway
      DeploymentId: !Ref MyDeployment
      MethodSettings:
        - ResourcePath: /
          HttpMethod: POST
          MetricsEnabled: 'true'
          DataTraceEnabled: 'false'

  MyDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: MyMethod
    Properties:
      RestApiId: !Ref MyApiGateway
#      StageName: !Ref ENVIRONMENT      # uncommenting this will cause the error
于 2021-12-06T18:29:29.490 回答