feat(api): containerization
- Build SQLx queries beforehand so that we don't have to do PostgreSQL init right away at service start up - Created `Dockerfile.production` - Updated docs - Seperate configuration files for local and development environments
This commit is contained in:
parent
7b5fa61780
commit
daf914bb8e
17
.sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json
generated
Normal file
17
.sqlx/query-4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b.json
generated
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n INSERT INTO subscriptions (id, email, name, subscribed_at)\n VALUES ($1, $2, $3, $4)\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid",
|
||||||
|
"Text",
|
||||||
|
"Text",
|
||||||
|
"Timestamptz"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "4bd6bbade521cd577279e91d8a8b978748046beff031d153699b351089c3bf9b"
|
||||||
|
}
|
20
Dockerfile.production
Normal file
20
Dockerfile.production
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# We use the latest Rust stable release as base image
|
||||||
|
FROM rust:1.78.0
|
||||||
|
# Let's switch our working directory to `app` (equivalent to `cd app`)
|
||||||
|
# The `app` folder will be created for us by Docker in case it does not
|
||||||
|
# exist already.
|
||||||
|
WORKDIR /app
|
||||||
|
# Install the required system dependencies for our linking configuration
|
||||||
|
RUN apt update && apt install lld clang -y
|
||||||
|
|
||||||
|
# Copy all files from our working environment to our Docker image
|
||||||
|
COPY . .
|
||||||
|
# Let's build our binary!
|
||||||
|
# We'll use the release profile to make it faaaast
|
||||||
|
ENV SQLX_OFFLINE true
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
ENV APP_ENVIRONMENT production
|
||||||
|
|
||||||
|
# When `docker run` is executed, launch the binary!
|
||||||
|
ENTRYPOINT ["./target/release/email_newsletter_api"]
|
@ -9,4 +9,12 @@
|
|||||||
- Run `cargo watch -x check -x test -x run` to lint, test and run the binary as soon as you make a change to the file.
|
- Run `cargo watch -x check -x test -x run` to lint, test and run the binary as soon as you make a change to the file.
|
||||||
- Bonus: install and use `mold`, a very fast linker that can link your Rust binary _blazingly fast_.
|
- Bonus: install and use `mold`, a very fast linker that can link your Rust binary _blazingly fast_.
|
||||||
|
|
||||||
|
## Notable Dependencies
|
||||||
|
|
||||||
|
- `actix-web`: Most popular Rust web framework
|
||||||
|
- `serde`: Data structure serialization/deserialization
|
||||||
|
- `tokio`: Async Runtime
|
||||||
|
- `tracing`: Alternative to traditional logging
|
||||||
|
- `sqlx`: SQL toolkit for Rust. Offers compile-time SQL checked queries
|
||||||
|
|
||||||
## [Technical Write Up](./docs/technical_write_up.md)
|
## [Technical Write Up](./docs/technical_write_up.md)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
application_port: 8000
|
application:
|
||||||
|
port: 8000
|
||||||
|
host: 0.0.0.0
|
||||||
database:
|
database:
|
||||||
host: "127.0.0.1"
|
host: "localhost"
|
||||||
port: 5432
|
port: 5432
|
||||||
username: "postgres"
|
username: "postgres"
|
||||||
password: "password"
|
password: "password"
|
2
configuration/local.yaml
Normal file
2
configuration/local.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
application:
|
||||||
|
host: 127.0.0.1
|
2
configuration/production.yaml
Normal file
2
configuration/production.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
application:
|
||||||
|
host: 0.0.0.0
|
@ -3,3 +3,8 @@
|
|||||||
## SQLx
|
## SQLx
|
||||||
|
|
||||||
The SQLx library will run compile time checks to make sure our SQL queries are valid. This is done by running PostgreSQL queries during compile time. Therefore, it is important that DATABASE_URL must be properly set.
|
The SQLx library will run compile time checks to make sure our SQL queries are valid. This is done by running PostgreSQL queries during compile time. Therefore, it is important that DATABASE_URL must be properly set.
|
||||||
|
|
||||||
|
### Offline mode vs Online mode
|
||||||
|
|
||||||
|
- Online mode is when the database is up and running and therefore, `SQLx` can perform compile time SQL queries check against it.
|
||||||
|
- Offline mode is when the database is NOT up and running. But we can save query metadata for offline usage and build to let the app run without SQLx complaining.
|
||||||
|
@ -3,7 +3,13 @@ use secrecy::{ExposeSecret, Secret};
|
|||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub database: DatabaseSettings,
|
pub database: DatabaseSettings,
|
||||||
pub application_port: u16,
|
pub application: ApplicationSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct ApplicationSettings {
|
||||||
|
pub port: u16,
|
||||||
|
pub host: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
@ -16,16 +22,56 @@ pub struct DatabaseSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
|
pub fn get_configuration() -> Result<Settings, config::ConfigError> {
|
||||||
|
let base_path = std::env::current_dir().expect("Failed to determine the current directory");
|
||||||
|
let configuration_directory = base_path.join("configuration");
|
||||||
|
|
||||||
|
let environment: Environment = std::env::var("APP_ENVIRONMENT")
|
||||||
|
.unwrap_or_else(|_| "local".into())
|
||||||
|
.try_into()
|
||||||
|
.expect("Failed to parse APP_ENVIRONMENT.");
|
||||||
|
|
||||||
|
let environment_filename = format!("{}.yaml", environment.as_str());
|
||||||
|
|
||||||
let settings = config::Config::builder()
|
let settings = config::Config::builder()
|
||||||
.add_source(config::File::new(
|
.add_source(config::File::from(
|
||||||
"configuration.yaml",
|
configuration_directory.join("base.yaml"),
|
||||||
config::FileFormat::Yaml,
|
))
|
||||||
|
.add_source(config::File::from(
|
||||||
|
configuration_directory.join(environment_filename),
|
||||||
))
|
))
|
||||||
.build()?;
|
.build()?;
|
||||||
|
|
||||||
settings.try_deserialize::<Settings>()
|
settings.try_deserialize::<Settings>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The possible runtime environment for our application.
|
||||||
|
pub enum Environment {
|
||||||
|
Local,
|
||||||
|
Production,
|
||||||
|
}
|
||||||
|
impl Environment {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Environment::Local => "local",
|
||||||
|
Environment::Production => "production",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryFrom<String> for Environment {
|
||||||
|
type Error = String;
|
||||||
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"local" => Ok(Self::Local),
|
||||||
|
"production" => Ok(Self::Production),
|
||||||
|
other => Err(format!(
|
||||||
|
"{} is not a supported environment. \
|
||||||
|
Use either `local` or `production`.",
|
||||||
|
other
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DatabaseSettings {
|
impl DatabaseSettings {
|
||||||
pub fn connection_string(&self) -> Secret<String> {
|
pub fn connection_string(&self) -> Secret<String> {
|
||||||
Secret::new(format!(
|
Secret::new(format!(
|
||||||
|
17
src/main.rs
17
src/main.rs
@ -16,14 +16,19 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
);
|
);
|
||||||
init_subscriber(subscriber);
|
init_subscriber(subscriber);
|
||||||
|
|
||||||
let db_conn = PgPool::connect(configuration.database.connection_string().expose_secret())
|
let db_conn = PgPool::connect_lazy(configuration.database.connection_string().expose_secret())
|
||||||
.await
|
|
||||||
.expect("Failed to connect to PostgreSQL");
|
.expect("Failed to connect to PostgreSQL");
|
||||||
|
|
||||||
let port_number = configuration.application_port;
|
let listener = TcpListener::bind(format!(
|
||||||
|
"{}:{}",
|
||||||
let listener = TcpListener::bind(format!("127.0.0.1:{}", port_number))
|
configuration.application.host, configuration.application.port
|
||||||
.unwrap_or_else(|_| panic!("Can't bind to port {} at localhost", port_number));
|
))
|
||||||
|
.unwrap_or_else(|_| {
|
||||||
|
panic!(
|
||||||
|
"Can't bind to port {} at localhost",
|
||||||
|
configuration.application.port
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
// Move the error up the call stack
|
// Move the error up the call stack
|
||||||
// otherwise await for the HttpServer
|
// otherwise await for the HttpServer
|
||||||
|
Loading…
x
Reference in New Issue
Block a user