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:
- BeforeInstall
- AfterInstall
- AfterAllowTestTraffic
- BeforeAllowTraffic
- 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.
Congratulations
And that's it! Congratulations, you now know how to implement a CodeDeploy LifeCycle Hook in Lambda with Rust!