'AWS sam deploy hangs on pretraffic hook
I'm learning AWS SAM (serverless) and trying to configure a pretraffic hook function that would do things like database migration. I'm following this example in JavaScript:
docs: https://github.com/aws/serverless-application-model/blob/master/docs/safe_lambda_deployments.rst
JavaScript handler: https://github.com/aws/serverless-application-model/blob/d168f371f494196a57032313075db9faae5587e4/examples/2016-10-31/lambda_safe_deployments/src/preTrafficHook.js
It's hanging when it gets to the part where it would call the pretraffic hook, but never invokes it. The hook function name starts with CodeDeployHook_ so the role has permissions to invoke it. (I know because I previously missed this and got a permissions error.)
Is there any way to see where and why it's hanging? I don't see this in the CodeDeploy console, it just gets stuck at "Pre-deployment validation." (It says 50% complete during deploy, but when I stop and rollback it shows only 1% complete).
I can manually invoke the function via the Test tab of the console.
There could be an issue with my function code, as there is no handler interface to implement for CodeDeploy hooks that would ensure the correct signature. But CodeDeploy SDK does have objects for the request/response that match what is in the JS file. Here is my code:
class PreTrafficFunction implements RequestHandler {
private static final Logger logger = LoggerFactory.getLogger(PreTrafficFunction.class);
/**
* Performs logic required before traffic is routed to a Lambda
*
* @param input a PutLifecycleEventHookExecutionStatusRequest
* @param context The Lambda execution environment context object.
* @return The Lambda Function output
*/
public Object handleRequest(Object input, Context context) {
PutLifecycleEventHookExecutionStatusRequest event = (PutLifecycleEventHookExecutionStatusRequest) input;
logger.info(event.toString());
AmazonCodeDeploy cd = AmazonCodeDeployClientBuilder.standard().build();
return cd.putLifecycleEventHookExecutionStatus(event.withStatus("Succeeded"));
}
}
template.yaml:
Parameters:
# Only reasonable way to have multiple environments is entirely separate CFN stacks for each
Env:
Type: String
Default: Dev
AllowedValues:
- Dev
- Prod
- Local
Description: Enter Local, Dev, or Prod - Dev is the default.
Globals:
Api:
# This is only necessary to prevent SAM from creating an unnecessary stage named "Stage" that you have to delete later.
OpenApiVersion: 3.0.1
Function:
Timeout: 300
Runtime: java11
Architectures:
- x86_64
Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
Variables:
APP_ENV: !Ref Env
Resources:
# Create one API Gateway that the rest use
RestApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Env
DBTesterFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: LambdaZipFunction
Handler: lambdazip.DBTester::handleRequest
AutoPublishAlias: !Ref Env
MemorySize: 512
Events:
DBTester:
# API Gateway: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-api.html
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /db
Method: get
RestApiId:
Ref: RestApi
DeploymentPreference:
# Point here is to run pre-traffic function to create/migrate DB tables before traffic hits
Type: AllAtOnce
Hooks:
# Validation Lambda functions that are run before & after traffic shifting
PreTraffic: !Ref PreTrafficFunction
PreTrafficFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: LambdaZipFunction
Handler: lambdazip.PreTrafficFunction::handleRequest
MemorySize: 512
FunctionName: 'CodeDeployHook_preTrafficHook'
DeploymentPreference:
Enabled: False
Role: ""
Policies:
- Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "codedeploy:PutLifecycleEventHookExecutionStatus"
Resource:
!Sub 'arn:${AWS::Partition}:codedeploy:${AWS::Region}:${AWS::AccountId}:deploymentgroup:${ServerlessDeploymentApplication}/*'
- Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- "lambda:InvokeFunction"
Resource: !GetAtt DBTesterFunction.Arn
Environment:
Variables:
CurrentVersion: !Ref DBTesterFunction.Version
Solution 1:[1]
Few things got in the way of this.
First, as mentioned I initially omitted the function name, which is required by the IAM role. For that, I saw an error in the CodeDeploy console for the deploy.
I added this and the permissions error went away, but it still did not invoke the function (or didn't appear to). This is where it hung as described above.
I had to comment out the hook section and redeploy to allow that part of the change set to finish and change the name of the function. Adding the hook section back, I could now see CloudWatch logs for that function. It appears it tries a few times and stops, or perhaps it is using the standard decaying retries.
I added log messages to show the class and properties of the input object. Turns out, it's not being marshalled to a PutLifecycleEventHookExecutionStatusRequest (which would make the most sense) or even a normal object with fields, but rather a LinkedHashMap. Knowing that, I could process the input to create the status request and submit it to CodeDeploy. Here is my working class in Groovy - trivial to convert to Java:
@CompileStatic
class PreTrafficFunction implements RequestHandler {
private static final Logger logger = LoggerFactory.getLogger(PreTrafficFunction.class)
/**
* Performs logic required before traffic is routed to a Lambda.
*
* @param input a Map with the fields of a PutLifecycleEventHookExecutionStatusRequest
* @param context The Lambda execution environment context object.
* @return PutLifecycleEventHookExecutionStatusResult
*/
Object handleRequest(Object input, Context context) {
logger.info("Input properties:\n${input.properties}\nInput:\n${input?.toString()}")
Map<String, String> event = input as Map<String, String>
if (!input) {
throw new IllegalArgumentException("Input is null or empty")
}
PutLifecycleEventHookExecutionStatusRequest statusRequest = new PutLifecycleEventHookExecutionStatusRequest()
.withDeploymentId(event.DeploymentId)
.withLifecycleEventHookExecutionId(event.LifecycleEventHookExecutionId)
.withStatus('Succeeded')
AmazonCodeDeploy cd = AmazonCodeDeployClientBuilder.standard().build()
cd.putLifecycleEventHookExecutionStatus(statusRequest)
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Philip |
