Skip to content

Commit d25460b

Browse files
committed
3. Deploy to AWS Lambda
1 parent d53798b commit d25460b

File tree

9 files changed

+299
-3
lines changed

9 files changed

+299
-3
lines changed

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"cSpell.words": [
3+
"awscli",
4+
"awscliv",
35
"github",
6+
"Ijoi",
47
"Korolev",
58
"peaceiris",
69
"serde",

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ members = [
55
"app/functions/function_one",
66
"app/functions/function_two",
77
"app/functions/function_three",
8+
"app/functions/function_four",
89
# libraries
9-
"app/libraries/lambda_http_wrapper",
10+
"app/libraries/lambda_http_wrapper", "app/functions/function_four",
1011
]
1112

1213
[workspace.dependencies]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "function_four"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
tokio = { workspace = true }
8+
serde = { workspace = true }
9+
10+
# internal dependencies
11+
lambda_http_wrapper = { path = "../../libraries/lambda_http_wrapper"}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use crate::types::*;
2+
3+
/// This is “business logic” controller
4+
pub(crate) async fn handle(req: Request) -> Result<Response, ErrorResponse> {
5+
// For example, get a "name" from the query string (defaulting to "world")
6+
let name = req.name;
7+
8+
// Here you would call into your service layer, etc.
9+
Ok(Response {
10+
message: format!("[Function_4] Hello {}, this is an AWS Lambda HTTP request using controller wrapper to avoid lots of boilerplate", name),
11+
})
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use lambda_http_wrapper::Error;
2+
3+
use lambda_http_wrapper::run;
4+
5+
mod controller;
6+
use controller::handle;
7+
mod types;
8+
9+
#[tokio::main]
10+
async fn main() -> Result<(), Error> {
11+
run(handle).await
12+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//! This module defines the “API types” that your controllers use.
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
/// Your application’s Request – you can add more fields as needed (e.g. headers, body).
6+
#[derive(Debug, Serialize, Deserialize)]
7+
pub struct Request {
8+
/// For simplicity we extract just the query parameters.
9+
pub name: String,
10+
}
11+
12+
/// Your successful Response.
13+
#[derive(Debug, Serialize, Deserialize)]
14+
pub struct Response {
15+
pub message: String,
16+
}
17+
18+
/// Your Error Response.
19+
#[derive(Debug, Serialize, Deserialize)]
20+
pub struct ErrorResponse {
21+
pub error: String,
22+
}

doc/.vitepress/config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ export default {
2828
text: "Tutorial",
2929
collapsible: true,
3030
items: [
31-
{ text: "01 - Initial Setup", link: "/tutorial/01_initial_setup" },
31+
{ text: "1. Initial Setup", link: "/tutorial/01_initial_setup" },
3232
{
33-
text: "02 - Handling REST API Requests",
33+
text: "2. Handling REST API Requests",
3434
link: "/tutorial/02_handle_rest_requests",
3535
},
36+
{ text: "3. Deploy to AWS Lambda", link: "/tutorial/03_deploy_to_aws_lambda" },
3637
],
3738
},
3839
],
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
# 3. Deploy to AWS Lambda
2+
3+
In Lesson 1/2 I created a fully working setup for comfortable development on my local machine. (No database yet – that comes in the next lesson.)
4+
5+
Let's deploy everything to AWS Lambda now!
6+
7+
---
8+
9+
## Off-topic
10+
11+
But first let me talk a little bit about my current setup.
12+
13+
As you noticed, I do not use the default Rust/Cargo bin/lib project entry points. Instead, I define my own custom main source files. This introduces a sort of duplication in the `Cargo.toml` files, which isn’t that bad; however, some may prefer to stick with Rust/Cargo defaults. So I will create an additional example function called `function_four` to illustrate an alternative approach.
14+
15+
```bash
16+
cd app/functions/
17+
cargo new function_four
18+
```
19+
20+
`app/functions/function_four/Cargo.toml`
21+
22+
```toml
23+
[package]
24+
name = "function_four"
25+
version = "0.1.0"
26+
edition = "2021"
27+
28+
[dependencies]
29+
tokio = { workspace = true }
30+
serde = { workspace = true }
31+
32+
# internal dependencies
33+
lambda_http_wrapper = { path = "../../libraries/lambda_http_wrapper"}
34+
```
35+
36+
`app/functions/function_four/src/main.rs`
37+
38+
```rust
39+
use lambda_http_wrapper::Error;
40+
41+
use lambda_http_wrapper::run;
42+
43+
mod controller;
44+
use controller::handle;
45+
mod types;
46+
47+
#[tokio::main]
48+
async fn main() -> Result<(), Error> {
49+
run(handle).await
50+
}
51+
```
52+
53+
`app/functions/function_four/src/types.rs`
54+
55+
```rust
56+
//! This module defines the “API types” that your controllers use.
57+
58+
use serde::{Deserialize, Serialize};
59+
60+
/// Your application’s Request – you can add more fields as needed (e.g. headers, body).
61+
#[derive(Debug, Serialize, Deserialize)]
62+
pub struct Request {
63+
/// For simplicity we extract just the query parameters.
64+
pub name: String,
65+
}
66+
67+
/// Your successful Response.
68+
#[derive(Debug, Serialize, Deserialize)]
69+
pub struct Response {
70+
pub message: String,
71+
}
72+
73+
/// Your Error Response.
74+
#[derive(Debug, Serialize, Deserialize)]
75+
pub struct ErrorResponse {
76+
pub error: String,
77+
}
78+
```
79+
80+
`app/functions/function_four/src/controller.rs`
81+
82+
```rust
83+
use crate::types::*;
84+
85+
/// This is “business logic” controller
86+
pub(crate) async fn handle(req: Request) -> Result<Response, ErrorResponse> {
87+
// For example, get a "name" from the query string (defaulting to "world")
88+
let name = req.name;
89+
90+
// Here you would call into your service layer, etc.
91+
Ok(Response {
92+
message: format!("[Function_4] Hello {}, this is an AWS Lambda HTTP request using controller wrapper to avoid lots of boilerplate", name),
93+
})
94+
}
95+
```
96+
97+
This approach has its benefits: next time you want to add another Lambda function to your project, you simply copy an existing function’s directory, change the package name in its `Cargo.toml`, add it to the top-level workspace `Cargo.toml`, and (optionally) modify its request/response types and controller logic. You can choose between using the default approach or my fully custom approach — the choice is yours.
98+
99+
## Deploying to AWS Lambda using cargo-lambda
100+
101+
In project root:
102+
103+
```bash
104+
$ cargo lambda build --release
105+
106+
$ cargo lambda deploy function_one
107+
function deployed successfully 🎉
108+
🛠️ binary last compiled a minute ago
109+
🔍 arn: arn:aws:lambda:eu-central-1:014498641106:function:function_one
110+
🎭 version: 1
111+
112+
$ cargo lambda deploy function_three
113+
function deployed successfully 🎉
114+
🛠️ binary last compiled 12 minutes ago
115+
🔍 arn: arn:aws:lambda:eu-central-1:014498641106:function:function_three
116+
🎭 version: 1
117+
```
118+
119+
Perfect. The deployment tool takes care of building and uploading your functions to AWS Lambda.
120+
121+
Function cold startup time is about 35–50ms, while execution durations are around 1ms (typically billed as 2ms when rounded up). I can see the functions in the AWS Console and test them using the `apigateway-http-api-proxy` template. Note that when testing a Lambda function in the AWS Console, you must supply all the required internal details. For example, the API Gateway proxy template supplies a base64‑encoded body (which you can decode with [base64encode.org](https://www.base64encode.org/)):
122+
123+
```json
124+
...
125+
"body": "eyJuYW1lIjoiZnVuY3Rpb25fb25lIn0=",
126+
...
127+
```
128+
129+
Testing in the AWS Console yields a response like this:
130+
131+
```json
132+
{
133+
"statusCode": 200,
134+
"headers": {
135+
"content-type": "application/json"
136+
},
137+
"multiValueHeaders": {},
138+
"body": "{\"message\":\"[Function_1] Hello function_one, this is an AWS Lambda HTTP request using controller wrapper to avoid lots of boilerplate\"}",
139+
"isBase64Encoded": false,
140+
"cookies": []
141+
}
142+
```
143+
144+
This is also how it appears on the AWS Lambda Dashboard:
145+
146+
![img](images/lambda_function_test_on_aws.png)
147+
148+
## Configure AWS locally
149+
150+
For me, deploying to AWS works right away because I’ve used the AWS CLI many times and it’s already configured on my machine. If you’re setting this up for the first time and you don’t have the AWS CLI installed or configured yet, follow these steps:
151+
152+
### 1. Install AWS CLI
153+
154+
**macOS:**
155+
156+
If you have Homebrew installed, run:
157+
158+
```bash
159+
brew update && brew install awscli
160+
```
161+
162+
**Windows:**
163+
164+
Download the AWS CLI MSI installer from the [AWS CLI Installation page](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and run the installer.
165+
166+
**Linux:**
167+
168+
Download and install using the following commands:
169+
170+
```bash
171+
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
172+
unzip awscliv2.zip
173+
sudo ./aws/install
174+
```
175+
176+
After installation, verify the installation by running:
177+
178+
```bash
179+
aws --version
180+
```
181+
182+
You should see output similar to `aws-cli/2.x.x Python/3.x.x Linux/...`.
183+
184+
### 2. Obtain Your AWS Access Keys
185+
186+
1. Log In to the AWS Management Console:
187+
Go to AWS Console and log in.
188+
189+
1. Navigate to IAM:
190+
In the AWS Console, search for and open IAM (Identity and Access Management).
191+
192+
1. Create a New User (if needed):
193+
194+
- In the IAM dashboard, click on Users in the sidebar.
195+
- Click Add user.
196+
- Enter a username (e.g., aws-cli-user).
197+
- Select Programmatic access as the access type.
198+
- Click Next: Permissions.
199+
- Attach a policy such as AdministratorAccess for testing (for production, consider a policy with only the required permissions).
200+
- Click through to create the user.
201+
202+
1. Save Your Access Keys:
203+
After the user is created, you’ll see an Access key ID and a Secret access key. Save these credentials securely (you won’t be able to see the secret access key again).
204+
205+
### 3. Configure AWS CLI
206+
207+
Open your terminal and run:
208+
209+
```bash
210+
aws configure
211+
```
212+
213+
When prompted, enter the following:
214+
215+
- AWS Access Key ID: (your access key ID)
216+
- AWS Secret Access Key: (your secret access key)
217+
- Default region name: (e.g., eu-central-1 or the region you use)
218+
- Default output format: (e.g., json)
219+
220+
These settings are saved in `~/.aws/credentials` and `~/.aws/config`.
221+
222+
### 4. Test Your Configuration
223+
224+
Run a simple command to confirm that your AWS CLI is properly configured:
225+
226+
```bash
227+
aws sts get-caller-identity
228+
```
229+
230+
You should receive a JSON output with details about your AWS account.
231+
232+
Note: Keep your AWS credentials secure and never expose them publicly or commit them to version control.
233+
234+
Now you’re ready to deploy your Lambda functions using `cargo lambda deploy...` — the AWS CLI will supply the necessary credentials for authentication.
101 KB
Loading

0 commit comments

Comments
 (0)