Вероятный тайм-аут Terraform лямбда-вызова

Потратив на это 7 часов, я решил связаться с вами. Мне нужно обновить учетные данные в потоке терраформ. Поскольку секреты не должны находиться в файле состояния, я использую лямбда-функцию AWS для обновления секрета экземпляра RDS. Пароль передается через CLI.

   locals {
  db_password = tostring(var.db_user_password)
}

data "aws_lambda_invocation" "credentials_manager" {
  function_name = "credentials-manager"
  input = <<JSON
{
  "secretString": "{\"username\":\"${module.db_instance.db_user}\",\"password\":\"${local.db_password}\",\"dbInstanceIdentifier\":\"${module.db_instance.db_identifier}\"}",
  "secretId": "${module.db_instance.db_secret_id}",
   "storageId": "${module.db_instance.db_identifier}",
   "forcedMod": "${var.forced_mod}"
}
JSON

    depends_on = [
    module.db_instance.db_secret_id,
  ]
}


output "result" {
  description = "String result of Lambda execution"
  value       = jsondecode(data.aws_lambda_invocation.credentials_manager.result)
}

Чтобы убедиться, что статус экземпляра RDS «доступен», лямбда-функция также содержит официанта. Когда я вручную выполняю функцию, все работает как шарм. Но в терраформе это происходит не отсюда:

data.aws_lambda_invocation.credentials_manager: Refreshing state...

Однако, когда я смотрю на AWS Cloud Watch, я вижу, что лямбда-функция вызывается Terraform снова и снова.

Это политика лямбда:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1589715377799",
      "Action": [
        "rds:ModifyDBInstance",
        "rds:DescribeDBInstances"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

Лямбда-функция выглядит так:

const secretsManager = require('aws-sdk/clients/secretsmanager')
const rds = require('aws-sdk/clients/rds')
const elastiCache = require('aws-sdk/clients/elasticache')
const log = require('loglevel')


/////////////////////////////////////////
// ENVIRONMENT VARIABLES
/////////////////////////////////////////
const logLevel = process.env["LOG_LEVEL"];
const region = process.env["REGION"]


/////////////////////////////////////////
// CONFIGURE LOGGER
log.setLevel(logLevel);
let protocol = []

/////////////////////////////////////////


/////////////////////////////////////////
// DEFINE THE CLIENTS
const SM = new secretsManager({ region })
const RDS = new rds({ region })
const ELC = new elastiCache({region})
/////////////////////////////////////////


/////////////////////////////////////////
// FUNCTION DEFINITIONS
/////////////////////////////////////////


// HELPERS

/**
 * @function waitForSeconds
 * Set a custom waiter.
 * 
 * @param {int} milseconds      - the milliseconds to set as timeout.
 * 
 */

const waitForSeconds = (ms) => {
   return new Promise(resolve => setTimeout(resolve, ms))
}




// AWS SECRETS MANAGER FUNCTIONS

/**
 * @function UpdateSecretInSM
 * The function updates the secrect value in the corresponding secret.
 * 
 * @param {string} secretId      - The id of the secret located in AWS SecretsManager 
 * @param {string} secretString  - The value of the new secret
 * 
 */
const UpdateSecretInSM = async (secretId,secretString) => {

    const params = {SecretId: secretId, SecretString: secretString}



    try {
        const data = await SM.updateSecret(params).promise()
        log.info(`[INFO]: Password for ${secretId} successfully changed in Scecrets Manager!`)
        let success = {Timestamp: new Date().toISOString(),Func: 'UpdateSecretInSM', Message: `Secret for ${secretId} successfully changed!`}
        protocol.push(success)
        return
    } catch (err) {
        log.debug("[DEBUG]: Error: ", err.stack);
        let error = {Timestamp: new Date().toISOString(),Func: 'UpdateSecretInSM', Error: err.stack}
        protocol.push(error)
        return
    }
}





/**
 * @function GetSecretFromSM
 * The function retrieves the specified secret from AWS SecretsManager.
 * Returns the password.
 * 
 * @param {string} secretId   - secretId that is available in AWS SecretsManager
 * 
 */
const GetSecretFromSM = async (secretId) => {


    try {
        const data = await SM.getSecretValue({SecretId: secretId}).promise()
        log.debug("[DEBUG]: Secret: ", data);
        let success = {Timestamp: new Date().toISOString(),Func: 'GetSecretFromSM', Message: 'Secret from SecretsManager successfully received!'}
        protocol.push(success)
        const { SecretString } = data
        const password = JSON.parse(SecretString)
        return password.password

    } catch (err) {

        log.debug("[DEBUG]: Error: ", err.stack);
        let error = {Timestamp: new Date().toISOString(),Func: 'GetSecretFromSM', Error: err.stack}
        protocol.push(error)
        return
    }

}


// AWS RDS FUNCTIONS

/**
 * @function ChangeRDSSecret
 * Change the secret of the specified RDS instance.
 * 
 * @param {string} rdsId     - id of the RDS instance
 * @param {string} password  - new password
 * 
 */
const ChangeRDSSecret = async (rdsId,password) => {

    const params = {
        DBInstanceIdentifier: rdsId,
        MasterUserPassword: password
    }



    try {
        await RDS.modifyDBInstance(params).promise() 

        log.info(`[INFO]: Password for ${rdsId} successfully changed!`)
        let success = {Timestamp: new Date().toISOString(), Func: 'ChangeRDSSecret', Message: `Secret for ${rdsId} successfully changed!`}
        protocol.push(success)
        return
    } catch (err) {

        log.debug("[DEBUG]: Error: ", err.stack);
        let error = {Timestamp: new Date().toISOString(), Func: 'ChangeRDSSecret', Error: err.stack}
        protocol.push(error)
        return 

    }

}


const DescribeRDSInstance = async(id) => {


    const params = { DBInstanceIdentifier : id }

    const secondsToWait = 10000

    try {
        let pendingModifications = true

        while (pendingModifications == true) {
            log.info(`[INFO]: Checking modified values for ${id}`)

            let data = await RDS.describeDBInstances(params).promise()
            console.log(data)

            // Extract the 'PendingModifiedValues' object
            let myInstance = data['DBInstances']
            myInstance = myInstance[0]

            if (myInstance.DBInstanceStatus === "resetting-master-credentials") {
                log.info(`[INFO]:Password change is being processed!`)
                 pendingModifications = false

            }

            log.info(`[INFO]: Waiting for ${secondsToWait/1000} seconds!`)
            await waitForSeconds(secondsToWait)
        }


        let success = {Timestamp: new Date().toISOString(), Func: 'DescribeRDSInstance', Message: `${id} available again!`}
        protocol.push(success)
        return 

    } catch (err) {
         log.debug("[DEBUG]: Error: ", err.stack);
        let error = {Timestamp: new Date().toISOString(), Func: 'DescribeRDSInstance', Error: err.stack}
        protocol.push(error)
        return 

    }

}


const WaitRDSForAvailableState = async(id) => {


/**
 * @function WaitRDSForAvailableState
 * Wait for the instance to be available again.
 * 
 * @param {string} id           - id of the RDS instance
 *
 */
    const params = { DBInstanceIdentifier: id}


    try {
        log.info(`[INFO]: Waiting for ${id} to be available again!`)
        const data = await RDS.waitFor('dBInstanceAvailable', params).promise()

        log.info(`[INFO]: ${id} available again!`)
        let success = {Timestamp: new Date().toISOString(), Func: 'WaitRDSForAvailableState', Message: `${id} available again!`}
        protocol.push(success)
        return
    } catch (err) {
        log.debug("[DEBUG]: Error: ", err.stack);
        let error = {Timestamp: new Date().toISOString(), Func: 'WaitRDSForAvailableState', Error: err.stack}
        protocol.push(error)
        return 

    }


}


// AWS ELASTICACHE FUNCTIONS

// ... removed since they follow the same principle like RDS





/////////////////////////////////////////
// Lambda Handler
/////////////////////////////////////////


exports.handler = async (event,context,callback) => {

    protocol = []
    log.debug("[DEBUG]: Event:", event)
    log.debug("[DEBUG]: Context:", context)

    // Variable for the final message the lambda function will return
    let finalValue



    // Get the password and rds from terraform output
    const secretString = event.secretString // manual input
    const secretId = event.secretId // coming from secretesmanager
    const storageId = event.storageId // coming from db identifier
    const forcedMod = event.forcedMod // manual input


    // Extract the password from the passed secretString to for comparison
    const passedSecretStringJSON = JSON.parse(secretString)
    const passedSecretString = passedSecretStringJSON.password


    const currentSecret = await GetSecretFromSM(secretId)

    // Case if the password has already been updated
    if (currentSecret !== "ChangeMeViaScriptOrConsole" && passedSecretString === "ChangeMeViaScriptOrConsole") {
        log.debug("[DEBUG]: No change necessary.")
        finalValue = {timestamp: new Date().toISOString(), 
            message: 'Lambda function execution finished!', 
            summary: 'Password already updated. It is not "ChangeMeViaScriptOrConsole."'}

        return finalValue
    }   

    // Case if the a new password has not been set yet
    if (currentSecret === "ChangeMeViaScriptOrConsole" && passedSecretString === "ChangeMeViaScriptOrConsole") {
        finalValue = {timestamp: new Date().toISOString(), 
            message: 'Lambda function execution finished!', 
            summary: 'Password still "ChangeMeViaScriptOrConsole". Please change me!'}

        return finalValue
    }

//   Case if the passed password is equal to the stored password and if pw modification is enforced
    if (currentSecret === passedSecretString && forcedMod === "no") {
        finalValue = {timestamp: new Date().toISOString(), 
            message: 'Lambda function execution finished!', 
            summary: 'Stored password is the same as the passed one. No changes made!'}

        return finalValue
    }


    // Case for changing the password
    if (passedSecretString !== "ChangeMeViaScriptOrConsole") {

        // Update the secret in SM for the specified RDS Instances
        await UpdateSecretInSM(secretId,secretString)

        log.debug("[DEBUG]: Secret updated for: ", secretId)

        // Change the new secret vom SM
        const updatedSecret = await GetSecretFromSM(secretId)

        log.debug("[DEBUG]: Updated secret: ", updatedSecret)


        if (secretId.includes("rds")) {

            // Update RDS instance with new secret and wait for it to be available again
            await ChangeRDSSecret(storageId, updatedSecret)
            await DescribeRDSInstance(storageId)
            await WaitRDSForAvailableState(storageId)

        } else if (secretId.includes("elasticache")) {

          // ... removed since it is analogeous to RDS


        } else {

            protocol.push(`No corresponding Storage Id exists for ${secretId}. Please check the Secret Id/Name in the terraform configuration.`)
        }


        finalValue ={timestamp: new Date().toISOString(), 
            message: 'Lambda function execution finished!', 
            summary: protocol}

        return finalValue

    } else {

        finalValue = {timestamp: new Date().toISOString(), 
            message: 'Lambda function execution finished!', 
            summary: 'Nothing changed'}

        return finalValue
    }




}

Кто-нибудь знает, как решить или смягчить это поведение?

См. также:  понимание структур, указателей и определений типов
Понравилась статья? Поделиться с друзьями:
IT Шеф
Комментарии: 1
  1. WorkoutBuddy

    Не могли бы вы показать политику iam для вашей лямбда-функции? Насколько я понимаю, вам может не хватать этого ресурса aws_lambda_permission для вашей лямбда-функции. https://www.terraform.io/docs/providers/aws/r/lambda_permission.html

    {Версия: 2012-10-17, Заявление: [{Действие: sts: AssumeRole, Принципал: {Сервис: lambda.amazonaws.com}, Эффект: Разрешить, Sid:}]} Когда я запускаю скрипт терраформирования, я вижу эта лямбда-функция вызывается и также выполняет свои действия. Но результат никогда не возвращается, вместо этого терраформ снова запускает функцию. В функции реализованы официанты, то есть, например, он остается в цикле до тех пор, пока база данных не изменит свой статус с «resetting-credentials» на «доступный». person WorkoutBuddy; 15.05.2020

    Когда вы имеете в виду result never comes back, вы имеете в виду результат функцией или функция не может сохранить его в желаемом месте назначения. например S3. Вы также можете проверить, какие действия может выполнять лямбда на основе политики IAM. Не могли бы вы показать политику для лямбда-функции, потому что та, которую вы указали в этом комментарии, — это роль, которую я хотел бы видеть в политике. Спасибо person WorkoutBuddy; 16.05.2020

    Я имею в виду, что лямбда-функция выполняет свои задачи, когда я смотрю в Cloudwatch. Через некоторое время он завершит свои задачи, и результат должен вернуться в выходную переменную Terraform. Вместо этого лямбда-функция вызывается снова и снова. Когда я удаляю цикл while в лямбде, все работает. person WorkoutBuddy; 17.05.2020

    У меня была аналогичная проблема с некоторыми резервными копиями, это действительно был тайм-аут и повторные попытки. Чтобы проверить, что происходит, я настраиваю пункт назначения onFailure в функции Lambda, чтобы отправлять мне данные об ошибке по электронной почте, чтобы я мог убедиться, что происходит. Кроме того, вы можете просто увеличить тайм-аут, чтобы посмотреть, поможет ли это, по умолчанию — 3 секунды. person WorkoutBuddy; 08.01.2021

Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: