Why a CloudFormation stack is drifted on updating the template itself from the console?

0

My understanding is that a CloudFormation stack is drifted if a resource under it is changed and its actual state is deviated from what is defined in the template. A drift must never happen if the resource is changed through an update of the existing stack using a new template. I found a drift in the API Gateway Authorizer of a stack when it is only changed through a template update. Is this a potential bug in either CloudFormation or API Gateway Authorizer?

Steps to reproduce:

  1. Create a new CloudFormation stack using the following template. It will create an API Gateway Authorizer (my-http-api-authorizer) and a Congnito user pool client (my-client-1) among other things.
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "My stack",
  "Resources": {
    "LambdaExecutionRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "RoleName": "HelloLambdaRole",
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": "lambda.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        }
      }
    },
    "LambdaFunction": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "FunctionName": "my-lambda-function",
        "Runtime": "python3.12",
        "Handler": "index.my_handler",
        "Code": {
          "ZipFile": "def my_handler(event, context):\n  message = \"Hello Lambda World!\"\n  return message\n"
        },
        "Role": {
          "Fn::GetAtt": "LambdaExecutionRole.Arn"
        }
      }
    },
    "LambdaIntegration": {
      "Type": "AWS::ApiGatewayV2::Integration",
      "Properties": {
        "ApiId": {
          "Ref": "HttpApi"
        },
        "IntegrationType": "AWS_PROXY",
        "IntegrationUri": {
          "Fn::Sub": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunction}"
        },
        "PayloadFormatVersion": "2.0"
      }
    },
    "LambdaIntegrationPermission": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "FunctionName": {
          "Ref": "LambdaFunction"
        },
        "Action": "lambda:InvokeFunction",
        "Principal": "apigateway.amazonaws.com",
        "SourceArn": {
          "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApi}/*"
        }
      }
    },
    "CognitoUserPool": {
      "Type": "AWS::Cognito::UserPool",
      "Properties": {
        "UserPoolName": "userpool",
        "AdminCreateUserConfig": {
          "AllowAdminCreateUserOnly": true
        }
      }
    },
    "CognitoUserPoolClient1": {
      "Type": "AWS::Cognito::UserPoolClient",
      "DependsOn": "CognitoResourceServer",
      "Properties": {
        "ClientName": "my-client-1",
        "UserPoolId": {
          "Ref": "CognitoUserPool"
        },
        "GenerateSecret": true,
        "AllowedOAuthFlows": ["client_credentials"],
        "AllowedOAuthFlowsUserPoolClient": true,
        "AllowedOAuthScopes": ["my_res_server/read"]
      }
    },
    "CognitoResourceServer": {
      "Type": "AWS::Cognito::UserPoolResourceServer",
      "Properties": {
        "Identifier": "my_res_server",
        "Name": "MY-RESOURCE-SERVER",
        "UserPoolId": {
          "Ref": "CognitoUserPool"
        },
        "Scopes": [
          {
            "ScopeName": "read",
            "ScopeDescription": "read access"
          }
        ]
      }
    },
    "CognitoUserPoolDomain": {
      "Type": "AWS::Cognito::UserPoolDomain",
      "Properties": {
        "Domain": "my-userpool-domain",
        "UserPoolId": {
          "Ref": "CognitoUserPool"
        }
      }
    },
    "HttpApi": {
      "Type": "AWS::ApiGatewayV2::Api",
      "Properties": {
        "Name": "my-http-api",
        "ProtocolType": "HTTP"
      }
    },
    "Authorizer": {
      "Type": "AWS::ApiGatewayV2::Authorizer",
      "Properties": {
        "ApiId": {
          "Ref": "HttpApi"
        },
        "Name": "my-http-api-authorizer",
        "AuthorizerType": "JWT",
        "IdentitySource": ["$request.header.Authorization"],
        "AuthorizerUri": {
          "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations"
        },
        "JwtConfiguration": {
          "Audience": [{ "Ref": "CognitoUserPoolClient1" }],
          "Issuer": {
            "Fn::Sub": "https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}"
          }
        }
      }
    },
    "HttpApiRoute": {
      "Type": "AWS::ApiGatewayV2::Route",
      "Properties": {
        "ApiId": {
          "Ref": "HttpApi"
        },
        "RouteKey": "POST /{proxy+}",
        "AuthorizationType": "JWT",
        "AuthorizerId": {
          "Ref": "Authorizer"
        },
        "Target": {
          "Fn::Join": [
            "/",
            [
              "integrations",
              {
                "Ref": "LambdaIntegration"
              }
            ]
          ]
        }
      }
    },
    "HttpApiStage": {
      "Type": "AWS::ApiGatewayV2::Stage",
      "Properties": {
        "ApiId": {
          "Ref": "HttpApi"
        },
        "StageName": "prod",
        "AutoDeploy": true
      }
    }
  },
  "Outputs": {
    "CognitoUserPoolId": {
      "Value": {
        "Ref": "CognitoUserPool"
      }
    },
    "CognitoUserPoolDomain": {
      "Value": {
        "Ref": "CognitoUserPoolDomain"
      }
    },
    "TokenEndpoint": {
      "Value": {
        "Fn::Sub": "https://${CognitoUserPoolDomain}.auth.${AWS::Region}.amazoncognito.com/oauth2/token"
      }
    },
    "HttpApiEndpoint": {
      "Value": {
        "Fn::Sub": "https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/prod"
      }
    }
  }
}
  1. Update the same stack by choosing 'Replace existing template'. Here the delta is addition of one more Cognito user pool client (my-client-2) and addition of this new Audience to the API Gateway Authorizer (my-http-api-authorizer).
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "My stack",
  "Resources": {
    "LambdaExecutionRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "RoleName": "HelloLambdaRole",
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": "lambda.amazonaws.com"
              },
              "Action": "sts:AssumeRole"
            }
          ]
        }
      }
    },
    "LambdaFunction": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "FunctionName": "my-lambda-function",
        "Runtime": "python3.12",
        "Handler": "index.my_handler",
        "Code": {
          "ZipFile": "def my_handler(event, context):\n  message = \"Hello Lambda World!\"\n  return message\n"
        },
        "Role": {
          "Fn::GetAtt": "LambdaExecutionRole.Arn"
        }
      }
    },
    "LambdaIntegration": {
      "Type": "AWS::ApiGatewayV2::Integration",
      "Properties": {
        "ApiId": {
          "Ref": "HttpApi"
        },
        "IntegrationType": "AWS_PROXY",
        "IntegrationUri": {
          "Fn::Sub": "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${LambdaFunction}"
        },
        "PayloadFormatVersion": "2.0"
      }
    },
    "LambdaIntegrationPermission": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "FunctionName": {
          "Ref": "LambdaFunction"
        },
        "Action": "lambda:InvokeFunction",
        "Principal": "apigateway.amazonaws.com",
        "SourceArn": {
          "Fn::Sub": "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApi}/*"
        }
      }
    },
    "CognitoUserPool": {
      "Type": "AWS::Cognito::UserPool",
      "Properties": {
        "UserPoolName": "userpool",
        "AdminCreateUserConfig": {
          "AllowAdminCreateUserOnly": true
        }
      }
    },
    "CognitoUserPoolClient1": {
      "Type": "AWS::Cognito::UserPoolClient",
      "DependsOn": "CognitoResourceServer",
      "Properties": {
        "ClientName": "my-client-1",
        "UserPoolId": {
          "Ref": "CognitoUserPool"
        },
        "GenerateSecret": true,
        "AllowedOAuthFlows": ["client_credentials"],
        "AllowedOAuthFlowsUserPoolClient": true,
        "AllowedOAuthScopes": ["my_res_server/read"]
      }
    },
    "CognitoUserPoolClient2": {
      "Type": "AWS::Cognito::UserPoolClient",
      "DependsOn": "CognitoResourceServer",
      "Properties": {
        "ClientName": "my-client-2",
        "UserPoolId": {
          "Ref": "CognitoUserPool"
        },
        "GenerateSecret": true,
        "AllowedOAuthFlows": ["client_credentials"],
        "AllowedOAuthFlowsUserPoolClient": true,
        "AllowedOAuthScopes": ["my_res_server/read"]
      }
    },
    "CognitoResourceServer": {
      "Type": "AWS::Cognito::UserPoolResourceServer",
      "Properties": {
        "Identifier": "my_res_server",
        "Name": "MY-RESOURCE-SERVER",
        "UserPoolId": {
          "Ref": "CognitoUserPool"
        },
        "Scopes": [
          {
            "ScopeName": "read",
            "ScopeDescription": "read access"
          }
        ]
      }
    },
    "CognitoUserPoolDomain": {
      "Type": "AWS::Cognito::UserPoolDomain",
      "Properties": {
        "Domain": "my-userpool-domain",
        "UserPoolId": {
          "Ref": "CognitoUserPool"
        }
      }
    },
    "HttpApi": {
      "Type": "AWS::ApiGatewayV2::Api",
      "Properties": {
        "Name": "my-http-api",
        "ProtocolType": "HTTP"
      }
    },
    "Authorizer": {
      "Type": "AWS::ApiGatewayV2::Authorizer",
      "Properties": {
        "ApiId": {
          "Ref": "HttpApi"
        },
        "Name": "my-http-api-authorizer",
        "AuthorizerType": "JWT",
        "IdentitySource": ["$request.header.Authorization"],
        "AuthorizerUri": {
          "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations"
        },
        "JwtConfiguration": {
          "Audience": [
            { "Ref": "CognitoUserPoolClient1" },
            { "Ref": "CognitoUserPoolClient2" }
          ],
          "Issuer": {
            "Fn::Sub": "https://cognito-idp.${AWS::Region}.amazonaws.com/${CognitoUserPool}"
          }
        }
      }
    },
    "HttpApiRoute": {
      "Type": "AWS::ApiGatewayV2::Route",
      "Properties": {
        "ApiId": {
          "Ref": "HttpApi"
        },
        "RouteKey": "POST /{proxy+}",
        "AuthorizationType": "JWT",
        "AuthorizerId": {
          "Ref": "Authorizer"
        },
        "Target": {
          "Fn::Join": [
            "/",
            [
              "integrations",
              {
                "Ref": "LambdaIntegration"
              }
            ]
          ]
        }
      }
    },
    "HttpApiStage": {
      "Type": "AWS::ApiGatewayV2::Stage",
      "Properties": {
        "ApiId": {
          "Ref": "HttpApi"
        },
        "StageName": "prod",
        "AutoDeploy": true
      }
    }
  },
  "Outputs": {
    "CognitoUserPoolId": {
      "Value": {
        "Ref": "CognitoUserPool"
      }
    },
    "CognitoUserPoolDomain": {
      "Value": {
        "Ref": "CognitoUserPoolDomain"
      }
    },
    "TokenEndpoint": {
      "Value": {
        "Fn::Sub": "https://${CognitoUserPoolDomain}.auth.${AWS::Region}.amazoncognito.com/oauth2/token"
      }
    },
    "HttpApiEndpoint": {
      "Value": {
        "Fn::Sub": "https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/prod"
      }
    }
  }
}
  1. Once the update is completed, check the drift status and notice that the stack is drifted, the Authorizer drift status is MODIFIED. Select 'View drift details' of the Authorizer, it will display like the following image. Authorizer drift details The property "AuthorizerUri" is Expected but missing from the Actual box.

Is this a bug or am I doing something wrong?

Thanks, Malem

2개 답변
2
수락된 답변

The AuthorizerUri is REMOVE. This means that the AuthorizerUri will be deleted, so the Actual box will not contain it.

This behavior is because AuthorizerUri is a property used when AuthorizerType is REQUEST. Since your template is JWT, the AuthorizerUri is ignored.

Also, since the current value is -, the AuthorizerUri would have been ignored at the first run.

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-apigatewayv2-authorizer.html

profile picture
전문가
shibata
답변함 한 달 전
profile picture
전문가
검토됨 10일 전
  • Thank you @shibata for the answer. This solves the problem. But as the current value is "-" and the AuthorizerUri would have been ignored at the first run, I still wonder why the drift is not detected right after the stack creation, at this point both the Actual box and the Expected box contain the AuthorizerUri. CloudFormation only detects the drift after the stack update. I would expect that the drift be detected right after the stack creation. Do you agree?

1

Indeed! It's strange.

One possibility is that the value is retained internally at creation. Still, the conflicting property is erased at some point. Unfortunately, I have no insight.

profile picture
전문가
shibata
답변함 한 달 전
  • I think AWS should address this inconsistency. Updating the stack should not cause a drift as per definition.

로그인하지 않았습니다. 로그인해야 답변을 게시할 수 있습니다.

좋은 답변은 질문에 명확하게 답하고 건설적인 피드백을 제공하며 질문자의 전문적인 성장을 장려합니다.

질문 답변하기에 대한 가이드라인

관련 콘텐츠