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:
minhtrannhat 2024-05-10 19:38:07 -04:00
parent 7b5fa61780
commit daf914bb8e
Signed by: minhtrannhat
GPG Key ID: E13CFA85C53F8062
9 changed files with 119 additions and 12 deletions

View 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
View 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"]

View File

@ -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)

View File

@ -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
View File

@ -0,0 +1,2 @@
application:
host: 127.0.0.1

View File

@ -0,0 +1,2 @@
application:
host: 0.0.0.0

View File

@ -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.

View File

@ -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!(

View File

@ -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