Skip to content

Commit f370947

Browse files
authored
Merge pull request #16 from ServerlessDevelopers/feature/code-deploy
Pattern for using Rust with Lambda Function Hooks for CodeDeploy
2 parents 17cbef6 + 6aa5d83 commit f370947

File tree

6 files changed

+2797
-0
lines changed

6 files changed

+2797
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"label": "CI/CD",
3+
"position": 1,
4+
"link": {
5+
"type": "generated-index",
6+
"description": "Serverless CI/CD Patterns"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
![CodeDeploy Lambda Function Hooks](/img/patterns/ci-cd-patterns/code_deploy.png)
140+
141+
## Congratulations
142+
143+
And that's it! Congratulations, you now know how to implement a CodeDeploy LifeCycle Hook in Lambda with Rust!
Loading

0 commit comments

Comments
 (0)