Ошибка создания цели автомасштабирования приложения на AWS при использовании Terraform - определение `scalable_resource` для настраиваемого ʻaws_appautoscaling_target`

Цель

Я реализую решение для автоматического масштабирования потоков данных Kinesis.

Одно возможное решение, которому я следую, хорошо задокументировано в https://github.com/aws-samples/aws-application-auto-scaling-kinesis. Однако в примере кода используется шаблон yaml облачной информации. Я хочу определить то же самое, используя terraform.

История до сих пор

При попытке создать цель масштабирования для my_custom_resource,

resource "aws_appautoscaling_target" "my_custom_resource" {
  resource_id        = "https://${aws_api_gateway_rest_api.my_api.id}.execute-api.${var.region}.amazonaws.com/prod/scalableTargetDimensions/${var.stream}"
  scalable_dimension = "custom-resource:ResourceType:Property"
  service_namespace  = "custom-resource"
}

Все атрибуты созданы в соответствии с документами AWS Auto-Scaling.

Тот же ресурс создается с помощью CloudFormation в связанном репозитории AWS:

KinesisAutoScaling:
  Type: AWS::ApplicationAutoScaling::ScalableTarget
  DependsOn: LambdaScaler
  Properties:
    ResourceId: !Sub https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/prod/scalableTargetDimensions/${MyKinesisStream}
    ScalableDimension: 'custom-resource:ResourceType:Property'
    ServiceNamespace: custom-resource

Примечание. Неактуальные атрибуты опущены для краткости.

Проблема

terraform apply выдает следующую ошибку:

Ошибка: ошибка при создании цели автомасштабирования приложения:  
ValidationException: Ошибка проверки ресурса:  
https://k5df89sd23.execute-api.us-west-1.amazonaws.com/prod/scalableTargetDimensions/my-test-stream,  
масштабируемое измерение: custom-resource:ResourceType:Property.  
Причина: масштабируемый ресурс не найден

  в строке 9 application-autoscaling.tf в ресурсе "aws_appautoscaling_target" "my_custom_resource":  
   9: ресурс "aws_appautoscaling_target" "my_custom_resource" {

Что может быть не так в определении терраформы?


Я знаю, что Terraform поддерживает шаблоны CloudFormation, используя aws_cloudformation_stack - обходной путь, которого я искренне хочу избежать.

1 ответ

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Я знаю, что вопрос был задан некоторое время назад, но я все равно отвечаю на тот случай, если это может помочь кому-то еще... Это определенно помогло бы мне.


Предполагая, что вы пытаетесь воспроизвести содержимое , вот что, по моему мнению, может быть неправильным:

Конечно часть ответа: aws_appautoscaling_target.my_custom_resource.resource_id

Если вы создали aws_api_gateway_deploymentресурс для воспроизведения части MyApi из aws-sample, тогда вам повезло!
вы можете использовать это:

      resource "aws_appautoscaling_target" "my_custom_resource" {
  # ...
  resource_id = "${aws_api_gateway_deployment.gateway.invoke_url}/scalableTargetDimensions/${var.stream}"
  # ...

Если вам нужны подробности о том, как заставить это ^ работать, смотрите ниже...

Но имейте в виду, что этого может быть недостаточно!


Вероятно, это тоже часть решения: внутренние ссылки и разрешения, необходимые для создания

всегда предполагая, что вы следуете одному и тому ссылки, которую вы указали в вопросеже примеру и что вы реализовали большую часть его с ...

также

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: я не уверен в точной причине остальной части этого ответа. Подозреваю, что это как-то связано с требованиями внутреннего API сервиса AWS Auto Scaling.


Вкратце: все должно быть подключено и работать, прежде чем вы сможете зарегистрировать цель автомасштабирования.

Эта часть: из

README.mdэтого другого проекта, который охватывает интеграцию, которую вы пытаетесь достичь, намекает на это.

Длинная версия; Что мне пришлось исправить в своей реализации:

  1. Формат лямбда-ответа
  2. Настройки шлюза API и проводка
  3. Правильные разрешения IAM
  4. Lambda и APIGateway в хорошем рабочем состоянии

1. Убедитесь, что у вас правильный лямбда-ответ

Формат возвращаемого значения лямбды имеет решающее значение для создания aws_appautoscaling_target.
Это должно быть ТОЧНО :

          returningJson = {
      "actualCapacity": float(actualCapacity),
      "desiredCapacity": float(desiredCapacity),
      "dimensionName": resourceName,
      "resourceName": resourceName,
      "scalableTargetDimensionId": resourceName,
      "scalingStatus": scalingStatus,
      "version": "MyVersion"
    }
    
    try:
        returningJson['failureReason'] = failureReason
    except:
        pass

(именно так это определено в образце)...
В моей реализации я поиграл с этим (до того, как все было развернуто и сделано), думая, что смогу получить больше данных из вызова, для метрик и мониторинга...
Оказывается что в конце концов, когда все остальное было сделано, все, что мне нужно было сделать, это восстановить функцию возврата, и все это успешно подключилось.

2. Настройки и проводка шлюза API

Эта часть вызвала у меня затруднения. Я думаю, что это должно быть сделано правильно, чтобы API автоматического масштабирования подключался и находил цель, чтобы он мог ее зарегистрировать (это то, что делает ресурс, который вы пытаетесь создать).
Это определение API является допустимым openapi.yamlопределение.
Я предлагаю поместить это в файл, подобный этому:

openapi.yaml.template

      swagger: '2.0'
info:
  title: "${NAME}"
paths:
  '/scalableTargetDimensions/{scalableTargetDimensionId}':
    get:
      tags:
        - ScalableTargets
      x-tags:
        - tag: ScalableTargets
      security:
        - sigv4: []
      x-amazon-apigateway-any-method:
        produces:
          - application/json
        consumes:
          - application/json
      x-amazon-apigateway-integration:
        httpMethod: POST
        type: aws_proxy
        uri: ${INTEGRATION_URI}
        responses: {}
    patch:
      tags:
        - ScalableTargets
      x-tags:
        - tag: ScalableTargets
      security:
        - sigv4: []
      x-amazon-apigateway-any-method:
        security:
          - sigv4: []
        produces:
          - application/json
        consumes:
          - application/json
      x-amazon-apigateway-integration:
        httpMethod: POST
        type: aws_proxy
        uri: ${INTEGRATION_URI}
        responses: {}
securityDefinitions:
  sigv4:
    type: apiKey
    name: Authorization
    in: header
    x-amazon-apigateway-authtype: awsSigv4

И вы можете использовать его следующим образом:

      # API Gateway
resource "aws_api_gateway_rest_api" "gateway" {
  name = var.rest_api_name
  body = templatefile("${path.module}/openapi.yaml.template",
    {
      NAME            = var.rest_api_name,
      INTEGRATION_URI = var.integration_uri
    }
  )
}

resource "aws_api_gateway_deployment" "gateway" {
  depends_on = [
    aws_api_gateway_rest_api.gateway,
  ]

  lifecycle {
    create_before_destroy = true
  }

  rest_api_id = aws_api_gateway_rest_api.gateway.id
  stage_name  = var.stage_name
}

3. Правильные разрешения IAM

Разрешения, определенные в cloudformationшаблоны и те, которые вы можете создать с помощью terraformне являются точным совпадением и, похоже, нуждаются в некоторой настройке, чтобы интеграция заработала... (Я подозреваю, что это связано с некоторой магией AWS, но в целом я считаю, что перенос в терраформу относительно прост)
Итак, вот roleа также policiesВ итоге я создал:

      # Lambda
resource "aws_lambda_function" "lambda" {
  # ...
  role = aws_iam_role.kinesis_autoscaler_lambda_role.arn
  # ...
}
resource "aws_iam_role" "kinesis_autoscaler_lambda_role" {
  name               = "${var.env}-kinesis-scaler-lambda-role"
  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_lambda_permission" "kinesis_api" {
  statement_id  = "AllowKinesisAPIInvoke"
  function_name = aws_lambda_function.lambda.function_name
  action        = "lambda:InvokeFunction"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_deployment.gateway.execution_arn}/GET/scalableTargetDimensions/{scalableTargetDimensionId}"
}

resource "aws_lambda_permission" "kinesis_api_patch" {
  statement_id  = "AllowKinesisAPIPatchInvoke"
  function_name = aws_lambda_function.lambda.function_name
  action        = "lambda:InvokeFunction"
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_api_gateway_deployment.gateway.execution_arn}/PATCH/scalableTargetDimensions/{scalableTargetDimensionId}"
}

# Permissions
resource "aws_iam_policy" "lambda_access_stream" {
  name = "${var.stream_name}-access-stream-policy"
                                      
  policy = <<POLICY                 
{                           
  "Version": "2012-10-17",                                          
  "Statement": [                                              
    {                                               
      "Sid": "KinesisConsumerAccess",     
      "Effect": "Allow",                                  
      "Action": [                                     
        "kinesis:DescribeStreamConsumer"               
      ],                                               
      "Resource": "${aws_kinesis_stream.stream.arn}/consumer/*:*"
    },                                        
    {                                                                                                                  
      "Sid": "KinesisStreamAccess",      
      "Effect": "Allow",                                                  
      "Action": [                                             
        "kinesis:DescribeLimits",                               
        "kinesis:DescribeStream",                                                                                     
        "kinesis:DescribeStreamConsumer",               
        "kinesis:DescribeStreamSummary",                                                                               
        "kinesis:UpdateShardCount"                      
      ],                                                      
      "Resource": "${aws_kinesis_stream.stream.arn}" 
    },                               
    {                                                              
      "Sid": "SSMParameterStoreGet",                                                                                
      "Effect": "Allow",                                  
      "Action": [    
        "ssm:GetParameter"           
      ],                                           
      "Resource": [                      
        "${aws_ssm_parameter.number_of_shards.arn}",    
      ]                                                           
    },                                             
    {                                                     
      "Sid": "SSMParameterStorePut",                      
      "Effect": "Allow",                                                                                               
      "Action": [                            
        "ssm:PutParameter"                                    
      ],                                
      "Resource": [                                                         
        "${aws_ssm_parameter.number_of_shards.arn}"
      ]                          
    }                                  
  ]                                 
}                                    
POLICY                                
}                  
# I ended up splitting the policies for `module` reasons...                              
resource "aws_iam_policy_attachment" "attach_lambda_stream_access" {
  name = "${aws_iam_role.kinesis_autoscaler_lambda_role.name}_attach_lambda_stream_access"
  roles = [                                         
    aws_iam_role.kinesis_autoscaler_lambda_role.name                  
  ]                                                       
  policy_arn = aws_iam_policy.lambda_access_stream.arn
}       

Это последнее является важным. Это результат последних настроек, которые я сделал, чтобы лямбда заработала.

      resource "aws_iam_policy" "lambda_access_scaling" {
  name = "${var.stream_name}-lambda-access-scaling-policy"

  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "FindScalingPolicyARNAndAlarms",
      "Effect": "Allow",
      "Action": [
        "application-autoscaling:DescribeScalingPolicies",
        "cloudwatch:DescribeAlarms"
      ],
      "Resource": "*"
    },
    {
      "Sid": "UpdateAlarms",
      "Effect": "Allow",
      "Action": [
        "cloudwatch:PutMetricAlarm",
        "cloudwatch:DeleteAlarms"
      ],
      "Resource": [
        "${aws_cloudwatch_metric_alarm.alarm_out.arn}",
        "${aws_cloudwatch_metric_alarm.alarm_in.arn}"
      ]
    }
  ]
}
POLICY
}

resource "aws_iam_policy_attachment" "attach_scaling_access" {
  name = "${aws_iam_role.kinesis_autoscaler_lambda_role.name}_attach_scaling_access"
  roles = [
    aws_iam_role.kinesis_autoscaler_lambda_role.name
  ]
  policy_arn = aws_iam_policy.lambda_access_scaling.arn
}

Кроме того, вы хотите иметь правильные разрешения для custom_appautoscaling...

      # For reference:  
resource "aws_appautoscaling_target" "kinesis_stream" {
  min_capacity       = var.min_number_of_shard
  max_capacity       = var.max_number_of_shard
  resource_id        = "${aws_api_gateway_deployment.gateway.invoke_url}/scalableTargetDimensions/${var.stream_name}"           
  role_arn           = aws_iam_role.custom_appautoscaling_service_role.arn
  scalable_dimension = "custom-resource:ResourceType:Property"
  service_namespace  = "custom-resource"
                                                                   
  depends_on = [                                        
    aws_iam_policy_attachment.attach_base_policy,
  ]                                                     
                 
  lifecycle {                                        
    ignore_changes = [
      # This is because the "assume_role_policy" becomes the actual
      # Role "AWSServiceRoleForApplicationAutoScaling_CustomResource"                                               
      # at runtime and is always attemted to be recreated 
      role_arn,
    ]              
  }
}                         
          
# Actual policies:
 
resource "aws_iam_role" "custom_appautoscaling_service_role" {
  name               = "${var.stream_name}-assume-custom-resource"
  assume_role_policy = <<-EOF                   
  {        
    "Version": "2012-10-17",                            
    "Statement": [
      {                                      
        "Action": "sts:AssumeRole",
        "Principal": {
          "Service": "custom-resource.application-autoscaling.amazonaws.com"
        },                                       
        "Effect": "Allow",
        "Sid": ""  
      }
    ]                     
  }             
  EOF
}                           
                        
resource "aws_iam_policy" "base_policy" {
  name = "${var.stream_name}_base_policy"
                                 
  policy = <<POLICY
{                  
  "Version": "2012-10-17",                             
  "Statement": [                                     
    {  
      "Sid": "DescribeAlarms",
      "Effect": "Allow",
      "Action": [
        "cloudwatch:DescribeAlarms"
      ],
      "Resource": "*"
    },                                                             
    {                                                   
      "Sid": "InvokeApiGateway",
      "Effect": "Allow",                                
      "Action": [
        "execute-api:Invoke*"                        
      ],
      "Resource": [
        "${aws_api_gateway_deployment.gateway.execution_arn}/scalableTargetDimensions/${var.stream_name}"
      ]                                                   
    }
  ]                
}
POLICY                    
}               
     
resource "aws_iam_policy_attachment" "attach_base_policy" {
  name = "${var.stream_name}_attach_base_policy"
  roles = [      
    aws_iam_role.custom_appautoscaling_service_role.name  
  ]                                
  policy_arn = aws_iam_policy.base_policy.arn
}                    
      
resource "aws_iam_policy" "alarms_modification" {
  name = "${var.stream_name}_alarms_modification"
                        
  policy = <<POLICY
{                                   
  "Version": "2012-10-17",       
  "Statement": [
    {              
      "Sid": "UpdateAlarms",                           
      "Effect": "Allow",                             
      "Action": [
        "cloudwatch:PutMetricAlarm",
        "cloudwatch:DeleteAlarms"
      ],
      "Resource": [
        "${aws_cloudwatch_metric_alarm.alarm_out.arn}",
        "${aws_cloudwatch_metric_alarm.alarm_in.arn}"
      ]                                                       
    }                                                   
  ]        
}                       
POLICY
}                                                      
 
resource "aws_iam_policy_attachment" "attach_alarms_modification" {                        
  name = "${var.stream_name}_attach_alarms_modification"
  roles = [                                            
    aws_iam_role.custom_appautoscaling_service_role.name
  ]
  policy_arn = aws_iam_policy.alarms_modification.arn
}              

4. Lambda и APIGateway в хорошем рабочем состоянии

Наконец, если после всего этого все еще не работает, вы можете захотеть устранить неполадки с вашей лямбдой и APIGateway... В
итоге я использовал apigateway-aws-proxy Шаблон события в лямбда-консоли. Немного редактируем поля, чтобы это выглядело так:

      {
  "body": {
    "desiredCapacity": "1"
  },
  "resource": "/{proxy+}",
  "path": "/scalableTargetDimensions/my-custom-stream",
  "httpMethod": "PATCH",
...
  "requestContext" {
    ...
    "path": "/<STAGE_NAME_SEE_API_GATEWAY_DEPLOYMENT_RESOURCE>/scalableTargetDimensions/my-custom-stream",
    "resourcePath": "/{proxy+}",
    "httpMethod": "PATCH",
}

Также для этой части я создал base64encodedbody (потому что это то, как он передается через APIGateway, я думаю?) ...
В любом случае, я немного подправил лямбду, чтобы она принимала оба, таким образом можно узнать, действительно ли лямбда работает правильно.

      {
  "body": "eyJkZXNpcmVkQ2FwYWNpdHkiOiIyIn0=",
  "resource": "/{proxy+}",
  "path": "/scalableTargetDimensions/my-custom-stream",
  "httpMethod": "PATCH",
  "isBase64Encoded": true,
...

При этом вы сможете устранить неполадки лямбды, убедившись, что все разрешения предоставлены правильно.
Для устранения неполадок APIGateway вы всегда можете использовать postman. Он хорошо обрабатывает аутентификацию в AWS, поэтому, если у вас есть учетные данные с адекватным доступом к созданному вами ресурсу, вы сможете выполнить некоторые действия. GETа также PATCHчтобы вручную запустить APIGatewayи протестируйте эту часть интеграции.

Другие вопросы по тегам