Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ch5 deploy #3

Merged
merged 9 commits into from Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dockerignore
@@ -0,0 +1,6 @@
.env
target/
tests/
Dockerfile
scripts/
migrations/

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Expand Up @@ -21,6 +21,7 @@ config = "0.14.0"
log = "0.4.21"
secrecy = { version = "0.8.0", features = ["serde"] }
serde = { version = "1.0.197", features = ["derive"] }
serde-aux = "4.5.0"
tokio = { version = "1.36.0", features = ["macros", "rt-multi-thread"] }
tracing = { version = "0.1.40", features = ["log"] }
tracing-actix-web = "0.7.10"
Expand Down
19 changes: 19 additions & 0 deletions Dockerfile
@@ -0,0 +1,19 @@
FROM rust:1.77.0 as builder

WORKDIR /app
RUN apt update && apt install -y lld clang
COPY . .
ENV SQLX_OFFLINE true
RUN cargo build --release

FROM debian:bookworm-slim as runtime
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends openssl ca-certificates \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/zero2prod zero2prod
COPY configuration configuration
ENV APP_ENVIRONMENT production
ENTRYPOINT [ "./zero2prod" ]
6 changes: 6 additions & 0 deletions README.md
Expand Up @@ -30,3 +30,9 @@ To disable a clippy warning: `#[allow(clippy::lint_name)]`
- [ ] Understand all those tracing related crates (e.g., `tracing::instrument`).
- [ ] Wire tracing with open telemetry (`tracing-opentelemetry`).
- [ ] `tracing` v.s. `log`.


### Ch5

- [ ] Use a container image for the app. E.g. [Wolfi](https://github.com/wolfi-dev/).
- [ ] Deploy to cloudflare if possible.
30 changes: 30 additions & 0 deletions chef.Dockerfile
@@ -0,0 +1,30 @@
FROM lukemathwalker/cargo-chef:latest-rust-1.72.0 as chef
WORKDIR /app
RUN apt update && apt install -y lld clang

FROM chef as planner
COPY . .
# Compute a lock-like file for our project
RUN cargo chef prepare --recipe-path recipe.json

FROM chef as builder
COPY --from=planner /app/recipe.json recipe.json
# Build our project dependencies
RUN cargo chef cook --release --recipe-path recipe.json
# Up to this point, if our dependency tree stays the same,
# all layers should be cached.
COPY . .
ENV SQLX_OFFLINE true
RUN cargo build --release --bin zero2prod

FROM debian:bookworm-slim as runtime
RUN apt-get update -y \
&& apt-get install -y --no-install-recommends openssl ca-certificates \
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/zero2prod zero2prod
COPY configuration configuration
ENV APP_ENVIRONMENT production
ENTRYPOINT [ "./zero2prod" ]
3 changes: 2 additions & 1 deletion configuration.yaml → configuration/base.yaml
@@ -1,4 +1,5 @@
application_port: 8000
application:
port: 8000
database:
host: "localhost"
port: 5432
Expand Down
4 changes: 4 additions & 0 deletions configuration/local.yaml
@@ -0,0 +1,4 @@
application:
host: 127.0.0.1
database:
require_ssl: false
4 changes: 4 additions & 0 deletions configuration/production.yaml
@@ -0,0 +1,4 @@
application:
host: 0.0.0.0
database:
require_ssl: true
54 changes: 54 additions & 0 deletions spec.yaml
@@ -0,0 +1,54 @@
name: zero2prod
# See https://www.digitalocean.com/docs/app-platform/#regional-availability for the available options
# You can get region slugs from https://www.digitalocean.com/docs/platform/availability-matrix/
# `fra` stands for Frankfurt (Germany - EU)
region: fra
services:
- name: zero2prod
# Relative to the repository root
dockerfile_path: Dockerfile
source_dir: .
github:
branch: root-chapter-05
deploy_on_push: true
repo: djdongjin/zero2prod
# Active probe used by DigitalOcean's to ensure our application is healthy
health_check:
# The path to our health check endpoint! It turned out to be useful in the end!
http_path: /health_check
# The port the application will be listening on for incoming requests
# It should match what we specify in our configuration.yaml file!
http_port: 8000
# For production workloads we'd go for at least two!
instance_count: 1
# Let's keep the bill lean for now...
instance_size_slug: basic-xxs
# All incoming requests should be routed to our app
routes:
- path: /
envs:
- key: APP_DATABASE__USERNAME
scope: RUN_TIME
value: ${newsletter.USERNAME}
- key: APP_DATABASE__PASSWORD
scope: RUN_TIME
value: ${newsletter.PASSWORD}
- key: APP_DATABASE__HOST
scope: RUN_TIME
value: ${newsletter.HOSTNAME}
- key: APP_DATABASE__PORT
scope: RUN_TIME
value: ${newsletter.PORT}
- key: APP_DATABASE__DATABASE_NAME
scope: RUN_TIME
value: ${newsletter.DATABASE}
databases:
# PG = Postgres
- engine: PG
# Database name
name: newsletter
# Again, let's keep the bill lean
num_nodes: 1
size: db-s-dev-database
# Postgres version - using the latest here
version: "14"
99 changes: 77 additions & 22 deletions src/configuration.rs
@@ -1,50 +1,105 @@
use secrecy::{ExposeSecret, Secret};
use serde_aux::field_attributes::deserialize_number_from_string;
use sqlx::postgres::{PgConnectOptions, PgSslMode};
use sqlx::ConnectOptions;

#[derive(serde::Deserialize)]
pub struct Settings {
pub database: DatabseSettings,
pub application_port: u16,
pub application: ApplicationSettings,
}

#[derive(serde::Deserialize)]
pub struct DatabseSettings {
pub username: String,
pub password: Secret<String>,
pub host: String,
#[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16,
pub database_name: String,
// Determine if we demand the connection to be encrypted or not
pub require_ssl: bool,
}

#[derive(serde::Deserialize)]
pub struct ApplicationSettings {
pub host: String,
pub port: u16,
}

impl DatabseSettings {
pub fn connection_string(&self) -> Secret<String> {
Secret::new(format!(
"postgres://{}:{}@{}:{}/{}",
self.username,
self.password.expose_secret(),
self.host,
self.port,
self.database_name
))
pub fn without_db(&self) -> PgConnectOptions {
let ssl_mode = if self.require_ssl {
PgSslMode::Require
} else {
PgSslMode::Prefer
};

PgConnectOptions::new()
.username(&self.username)
.password(self.password.expose_secret())
.host(&self.host)
.port(self.port)
.ssl_mode(ssl_mode)
}

pub fn connection_string_without_db(&self) -> Secret<String> {
Secret::new(format!(
"postgres://{}:{}@{}:{}",
self.username,
self.password.expose_secret(),
self.host,
self.port
))
pub fn with_db(&self) -> PgConnectOptions {
let options = self.without_db().database(&self.database_name);
options.log_statements(tracing_log::log::LevelFilter::Trace)
}
}

pub fn get_configuration() -> Result<Settings, config::ConfigError> {
let base_path = std::env::current_dir().expect("Failed to determine the current directory");
let config_dir = 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()
.add_source(config::File::new(
"configuration.yaml",
config::FileFormat::Yaml,
))
.add_source(config::File::from(config_dir.join("base.yaml")))
.add_source(config::File::from(config_dir.join(environment_filename)))
// Add in settings from environment variables (with a prefix of APP and
// '__' as separator)
// E.g. `APP_APPLICATION__PORT=5001 would set `Settings.application.port`
.add_source(
config::Environment::with_prefix("APP")
.prefix_separator("_")
.separator("__"),
)
.build()?;

settings.try_deserialize::<Settings>()
}

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(value: String) -> Result<Self, Self::Error> {
match value.to_lowercase().as_str() {
"local" => Ok(Self::Local),
"production" => Ok(Self::Production),
other => Err(format!(
"{} is not a suppported environment. \
Use either `local` or `production`.",
other
)),
}
}
}
13 changes: 6 additions & 7 deletions src/main.rs
@@ -1,5 +1,4 @@
use secrecy::ExposeSecret;
use sqlx::PgPool;
use sqlx::postgres::PgPoolOptions;
use std::net::TcpListener;
use zero2prod::configuration::get_configuration;
use zero2prod::startup::run;
Expand All @@ -11,12 +10,12 @@ async fn main() -> Result<(), std::io::Error> {
init_subscriber(subscriber);

let configuration = get_configuration().expect("Failed to read configuration");
let address = format!("127.0.0.1:{}", configuration.application_port);
let address = format!(
"{}:{}",
configuration.application.host, configuration.application.port
);
let listener = TcpListener::bind(address)?;
let connection_pool =
PgPool::connect(configuration.database.connection_string().expose_secret())
.await
.expect("Failed to connect to Postgres.");
let connection_pool = PgPoolOptions::new().connect_lazy_with(configuration.database.with_db());

run(listener, connection_pool)?.await
}
12 changes: 5 additions & 7 deletions tests/health_check.rs
@@ -1,7 +1,6 @@
use std::net::TcpListener;

use once_cell::sync::Lazy;
use secrecy::ExposeSecret;
use sqlx::{Connection, Executor, PgConnection, PgPool};
use uuid::Uuid;
use zero2prod::configuration::{get_configuration, DatabseSettings};
Expand Down Expand Up @@ -51,18 +50,17 @@ async fn spawn_app() -> TestApp {

async fn configure_database(config: &DatabseSettings) -> PgPool {
// Create database
let mut connection =
PgConnection::connect(config.connection_string_without_db().expose_secret())
.await
.expect("Failed to connect to Postgres.");
let mut connection = PgConnection::connect_with(&config.without_db())
.await
.expect("Failed to connect to Postgres");

// Migrate database
connection
.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str())
.await
.expect("Failed to create database.");

let connection_pool = PgPool::connect(config.connection_string().expose_secret())
// Migrate database
let connection_pool = PgPool::connect_with(config.with_db())
.await
.expect("Failed to connect to Postgres.");

Expand Down