|
| 1 | +--- |
| 2 | +sidebar_position: 1 |
| 3 | +title: Code Deploy Lifecycle Hook |
| 4 | +description: Implementig a CodeDeploy LifeCycle Hook |
| 5 | +keywords: [rust,lambda,ci/cd,lifecycle,codedeploy] |
| 6 | +--- |
| 7 | + |
| 8 | +## Introduction |
| 9 | + |
| 10 | +[AWS CodeDeploy](https://aws.amazon.com/codedeploy/) is a fully managed deployment coordinator that provides flexiblity during the deployment lifecyle. It can be defined like this: |
| 11 | + |
| 12 | +> 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 |
| 13 | +
|
| 14 | +CodeDeploy provides 5 unique hooks that are implemented with a Lambda Function. They are: |
| 15 | + |
| 16 | +1. BeforeInstall |
| 17 | +2. AfterInstall |
| 18 | +3. AfterAllowTestTraffic |
| 19 | +4. BeforeAllowTraffic |
| 20 | +5. AfterAllowTraffic |
| 21 | + |
| 22 | +To read more in detail [here's the documentation](https://docs.aws.amazon.com/codedeploy/latest/userguide/reference-appspec-file-structure-hooks.html#reference-appspec-file-structure-hooks-list-ecs) |
| 23 | + |
| 24 | +## Sample Solution |
| 25 | + |
| 26 | +A template for this pattern can be found under the [./templates](https://github.com/serverlessdevelopers/serverless-rust/tree/main/templates/patterns/ci-cd-patterns/codedeploy-lifecycle-hook/) directory in the GitHub repo. You can use the template to get started building with CodeDeploy LifeCycle Hooks and Lambda. |
| 27 | + |
| 28 | +### Main Function |
| 29 | + |
| 30 | +<CH.Section> |
| 31 | +Rust programs start off with a [`main`](focus://2) function. The main function in this sample includes the [`Tokio`](focus://1) macro so that this main can run asynchronous code. |
| 32 | + |
| 33 | +The only piece of this function that is required is an environment variable named [`ALB_URL`](focus://11). The |
| 34 | +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. |
| 35 | + |
| 36 | +```rust |
| 37 | +#[tokio::main] |
| 38 | +async fn main() -> Result<(), Error> { |
| 39 | + tracing_subscriber::fmt() |
| 40 | + .with_max_level(tracing::Level::INFO) |
| 41 | + .json() |
| 42 | + .with_target(false) |
| 43 | + .without_time() |
| 44 | + .init(); |
| 45 | + |
| 46 | + |
| 47 | + let alb_url = std::env::var("ALB_URL").expect("ALB_URL must be set"); |
| 48 | + let alb_str = &alb_url.as_str(); |
| 49 | + |
| 50 | + run(service_fn( |
| 51 | + move |event: LambdaEvent<HashMap<String, String>>| async move { |
| 52 | + function_handler(alb_str, event).await |
| 53 | + }, |
| 54 | + )).await |
| 55 | +} |
| 56 | +``` |
| 57 | +</CH.Section> |
| 58 | + |
| 59 | +### Handler |
| 60 | + |
| 61 | +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](https://github.com/awslabs/aws-lambda-rust-runtime/blob/de822f9d870c21c06b504d218293099f691ced9f/lambda-events/src/event/codedeploy/mod.rs#L68) |
| 62 | + |
| 63 | +<CH.Section> |
| 64 | + |
| 65 | +Let's dig through what all is happening. |
| 66 | + |
| 67 | +The first part of this handler is fetching out the values from the payload. We need to use the [`deployment_id`](focus://2) and [`lifecycle_event_hook_execution_id`](focus://3) to signal back to the CodeDeploy execution whether this deployment should continue or fail. |
| 68 | + |
| 69 | +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. |
| 70 | + |
| 71 | +[`Line 11`](focus://11) shows a call to [`run_test`](focus://11[21:28]). 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`](focus://17) or [`Fail`](focus://19) the CodeDeploy deployment. |
| 72 | + |
| 73 | +That status will then be based back through the [`put_lifecycle_event_hook_execution_status`](focus://23:27) |
| 74 | + |
| 75 | +```rust |
| 76 | +async fn function_handler(alb_url: &str, event: LambdaEvent<HashMap<String, String>>) -> Result<(), Error> { |
| 77 | + let deployment_id = event.payload.get("DeploymentId").unwrap(); |
| 78 | + let lifecycle_event_hook_execution_id = event.payload.get("LifecycleEventHookExecutionId").unwrap(); |
| 79 | + |
| 80 | + let config = aws_config::load_from_env().await; |
| 81 | + let client = Client::new(&config); |
| 82 | + |
| 83 | + let mut passed = true; |
| 84 | + |
| 85 | + // replaces the "one" to the route that needs to be exercised |
| 86 | + if let Err(_) = run_test(alb_url, "one".to_string()).await { |
| 87 | + info!("Test on Route one failed, rolling back"); |
| 88 | + passed = false |
| 89 | + } |
| 90 | + |
| 91 | + let status = if passed { |
| 92 | + LifecycleEventStatus::Succeeded |
| 93 | + } else { |
| 94 | + LifecycleEventStatus::Failed |
| 95 | + }; |
| 96 | + |
| 97 | + let cloned = status.clone(); |
| 98 | + client.put_lifecycle_event_hook_execution_status() |
| 99 | + .deployment_id(deployment_id) |
| 100 | + .lifecycle_event_hook_execution_id(lifecycle_event_hook_execution_id) |
| 101 | + .status(status) |
| 102 | + .send().await?; |
| 103 | + |
| 104 | + info!("Wrapping up requests with a status of: {:?}", cloned); |
| 105 | + Ok(()) |
| 106 | +} |
| 107 | + |
| 108 | +``` |
| 109 | +</CH.Section> |
| 110 | + |
| 111 | +### Run Test Function |
| 112 | + |
| 113 | +<CH.Section> |
| 114 | +The [`run_test`](focus://1[10:17]) 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`](focus://9), the handler will consider the execution a success. Anything else, and an [`error`](focus://12) |
| 115 | + |
| 116 | +```rust |
| 117 | +async fn run_test(url: &str, path: String) -> Result<(), Error> { |
| 118 | + let request_url = format!("http://{url}/{path}", url = url, path = path); |
| 119 | + info!("{}", request_url); |
| 120 | + |
| 121 | + let timeout = Duration::new(2, 0); |
| 122 | + let client = ClientBuilder::new().timeout(timeout).build()?; |
| 123 | + let response = client.head(&request_url).send().await?; |
| 124 | + |
| 125 | + if response.status().is_success() { |
| 126 | + Ok(()) |
| 127 | + } else { |
| 128 | + Err(format!("Error: {}", response.status()).into()) |
| 129 | + } |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +</CH.Section> |
| 134 | + |
| 135 | +## Seeing it in Action |
| 136 | + |
| 137 | +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. |
| 138 | + |
| 139 | + |
| 140 | + |
| 141 | +## Congratulations |
| 142 | + |
| 143 | +And that's it! Congratulations, you now know how to implement a CodeDeploy LifeCycle Hook in Lambda with Rust! |
0 commit comments