Skip to main content

Code Deploy Lifecycle Hook

Introduction

AWS CodeDeploy is a fully managed deployment coordinator that provides flexiblity during the deployment lifecyle. It can be defined like this:

AWS CodeDeploy is a fully managed deployment service that automates software deployments to various compute services, such as Amazon Elastic Compute Cloud (EC2), Amazon Elastic Container Service (ECS), AWS Lambda, and your on-premises servers. Use CodeDeploy to automate software deployments, eliminating the need for error-prone manual operations. - AWS

CodeDeploy provides 5 unique hooks that are implemented with a Lambda Function. They are:

  1. BeforeInstall
  2. AfterInstall
  3. AfterAllowTestTraffic
  4. BeforeAllowTraffic
  5. AfterAllowTraffic

To read more in detail here's the documentation

Sample Solution

A template for this pattern can be found under the ./templates directory in the GitHub repo. You can use the template to get started building with CodeDeploy LifeCycle Hooks and Lambda.

Main Function

Rust programs start off with a main function. The main function in this sample includes the Tokio macro so that this main can run asynchronous code.

The only piece of this function that is required is an environment variable named ALB_URL. The purpose of that variable is to allow the function to read the application load balancer that it can send a request or series of requests to on the test target group.


#[tokio::main]
async fn main() -> Result<(), Error> {
tracing_subscriber::fmt()
.with_max_level(tracing::Level::INFO)
.json()
.with_target(false)
.without_time()
.init();
let alb_url = std::env::var("ALB_URL").expect("ALB_URL must be set");
let alb_str = &alb_url.as_str();
run(service_fn(
move |event: LambdaEvent<HashMap<String, String>>| async move {
function_handler(alb_str, event).await
},
)).await
}

Handler

Every time this function is triggered, it's going to receive a payload of HashMap<String, String>. As of this writing, the Rust Lambda Events project hasn't published the code that supports a strongly-typed struct. For reference, here is that code

Let's dig through what all is happening.

The first part of this handler is fetching out the values from the payload. We need to use the deployment_id and lifecycle_event_hook_execution_id to signal back to the CodeDeploy execution whether this deployment should continue or fail.

A quick note when looking at those two lines of code, I'm unwrapping the get operation. While I normally don't recommend this, I'm confident that AWS is goig to send me what I expect. If I was to test this with faulty payloads, you would get an exception.

Line 11 shows a call to run_test. We'll explore that function below but it's purpose is to run a path on the ALB_URL that was supplied through environment variables. Based on the output of that function, the handler will decide to either Succeed or Fail the CodeDeploy deployment.

That status will then be based back through the put_lifecycle_event_hook_execution_status


async fn function_handler(alb_url: &str, event: LambdaEvent<HashMap<String, String>>) -> Result<(), Error> {
let deployment_id = event.payload.get("DeploymentId").unwrap();
let lifecycle_event_hook_execution_id = event.payload.get("LifecycleEventHookExecutionId").unwrap();
let config = aws_config::load_from_env().await;
let client = Client::new(&config);
let mut passed = true;
// replaces the "one" to the route that needs to be exercised
if let Err(_) = run_test(alb_url, "one".to_string()).await {
info!("Test on Route one failed, rolling back");
passed = false
}
let status = if passed {
LifecycleEventStatus::Succeeded
} else {
LifecycleEventStatus::Failed
};
let cloned = status.clone();
client.put_lifecycle_event_hook_execution_status()
.deployment_id(deployment_id)
.lifecycle_event_hook_execution_id(lifecycle_event_hook_execution_id)
.status(status)
.send().await?;
info!("Wrapping up requests with a status of: {:?}", cloned);
Ok(())
}

Run Test Function

The run_test function accepts a url and path and then executes an HTTP request on the full URL built by those inputs. As long as the endpoint returns anything 2xx, the handler will consider the execution a success. Anything else, and an error


async fn run_test(url: &str, path: String) -> Result<(), Error> {
let request_url = format!("http://{url}/{path}", url = url, path = path);
info!("{}", request_url);
let timeout = Duration::new(2, 0);
let client = ClientBuilder::new().timeout(timeout).build()?;
let response = client.head(&request_url).send().await?;
if response.status().is_success() {
Ok(())
} else {
Err(format!("Error: {}", response.status()).into())
}
}

Seeing it in Action

With all of this in place and attached to a CodeDeploy, you'll see output like this. A CodeDeploy executing or skipping the hooks that have been defined just like the Lambda Function code above.

CodeDeploy Lambda Function Hooks

Congratulations

And that's it! Congratulations, you now know how to implement a CodeDeploy LifeCycle Hook in Lambda with Rust!