From ac08d5a7158a18fb1a634409da3b8d7ec9972c7e Mon Sep 17 00:00:00 2001 From: Tine Date: Fri, 1 Mar 2024 22:05:49 +0100 Subject: [PATCH] feat: working example --- Dockerfile | 2 +- README.md | 8 +- devbox.json | 4 +- devbox.lock | 41 +++++++++++ justfile | 34 ++++++++- src/app.py | 21 +++++- src/database.py | 28 +++++++ requirements.txt => src/requirements.txt | 2 +- src/templates/index.html | 10 +++ terraform/.terraform.lock.hcl | 19 +++++ terraform/main.tf | 93 +++++++++++++++++++++++- terraform/outputs.tf | 4 +- terraform/providers.tf | 8 +- terraform/variables.tf | 6 ++ 14 files changed, 263 insertions(+), 17 deletions(-) create mode 100644 src/database.py rename requirements.txt => src/requirements.txt (73%) create mode 100644 src/templates/index.html diff --git a/Dockerfile b/Dockerfile index 6b320ce..27dfa88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM python:3.10-alpine WORKDIR /app -COPY requirements.txt /app +COPY src/requirements.txt /app RUN pip3 install -r requirements.txt COPY src /app diff --git a/README.md b/README.md index 1160747..5b3b658 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,12 @@ cp example.env .env just run # Provision infrastructure -just terraform apply +just deploy -# Deploy new code +## RELEASE PROCESS +# Release new code +just release +# At this point, you have to modify .env to point to new image +# And then we can deploy the new image. just deploy ``` diff --git a/devbox.json b/devbox.json index f3663df..fd473eb 100644 --- a/devbox.json +++ b/devbox.json @@ -5,7 +5,9 @@ "python312Packages.pip@latest", "terraform@latest", "azure-cli@latest", - "azure-functions-core-tools@latest" + "azure-functions-core-tools@latest", + "black@latest", + "postgresql@latest" ], "env": { "VENV_DIR": ".venv" diff --git a/devbox.lock b/devbox.lock index 48a9a2e..64ae8b4 100644 --- a/devbox.lock +++ b/devbox.lock @@ -38,6 +38,26 @@ } } }, + "black@latest": { + "last_modified": "2024-02-26T19:46:43Z", + "resolved": "github:NixOS/nixpkgs/548a86b335d7ecd8b57ec617781f5e652ab0c38e#black", + "source": "devbox-search", + "version": "23.11.0", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/4aks55v8kdck409q1ga0qkdnrphabv53-python3.11-black-23.11.0" + }, + "aarch64-linux": { + "store_path": "/nix/store/y7kcifhk0v58pfhqdmpdbpd0yxwv860z-python3.11-black-23.11.0" + }, + "x86_64-darwin": { + "store_path": "/nix/store/41jmbap8q6gsjs8g59xwr6f222pdzpp8-python3.11-black-23.11.0" + }, + "x86_64-linux": { + "store_path": "/nix/store/g8k9vjxgsy8d3dizjw1jlcmj2iphy512-python3.11-black-23.11.0" + } + } + }, "just@latest": { "last_modified": "2024-02-26T19:46:43Z", "resolved": "github:NixOS/nixpkgs/548a86b335d7ecd8b57ec617781f5e652ab0c38e#just", @@ -58,6 +78,27 @@ } } }, + "postgresql@latest": { + "last_modified": "2024-02-26T19:46:43Z", + "plugin_version": "0.0.2", + "resolved": "github:NixOS/nixpkgs/548a86b335d7ecd8b57ec617781f5e652ab0c38e#postgresql", + "source": "devbox-search", + "version": "15.6", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/j5m8ndrxpmk1444lzdk6p33vfzwrg6l7-postgresql-15.6" + }, + "aarch64-linux": { + "store_path": "/nix/store/3gbmk22frf8l07k3mhb16d43dy7bfd2s-postgresql-15.6" + }, + "x86_64-darwin": { + "store_path": "/nix/store/3a6lihh8nk9fvhf87vwkszfknjx27yfb-postgresql-15.6" + }, + "x86_64-linux": { + "store_path": "/nix/store/84xh621k32sc7ykqx37vcqdpjz9zxmdf-postgresql-15.6" + } + } + }, "python312Packages.pip@latest": { "last_modified": "2024-02-26T19:46:43Z", "plugin_version": "0.0.2", diff --git a/justfile b/justfile index 7e08cf6..1a75c15 100644 --- a/justfile +++ b/justfile @@ -4,10 +4,17 @@ set shell := ["devbox", "run"] set dotenv-load export TF_VAR_name := env("APP_NAME") +export GIT_SHA := `git rev-parse --short HEAD` # Run server locally run: - flask --app src/server run + initdb --username=postgres || true + devbox services start postgresql + python src/app.py + +run-docker: + docker build -t local/${APP_NAME}:${GIT_SHA} . + docker run -it --rm -p 8080:8080 local/${APP_NAME}:${GIT_SHA} dependencies: pip install -r src/requirements.txt @@ -15,11 +22,30 @@ dependencies: dependencies-lock: pip freeze -l > src/requirements.txt -deploy: +release: + #!/bin/env bash + USERNAME="00000000-0000-0000-0000-000000000000" + REGISTRY=$(terraform -chdir=terraform output -raw container_registry_name) -terraform-apply: + TAG=${REGISTRY}.azurecr.io/develop:${GIT_SHA}-$(date +"%F-%H-%M-%S") + + docker build -t ${TAG} . + + # For podman support we must use this hacks, + # otherwise az acr login would also do the docker login. + docker login \ + --username=${USERNAME} \ + --password=$(az acr login --name ${REGISTRY} --expose-token 2>/dev/null | jq -r '.accessToken') \ + "${REGISTRY}.azurecr.io" + + docker push ${TAG} + + echo "Image pushed to ${TAG}" + echo "Modify your .env with TF_VAR_image=${TAG}" + +deploy: terraform -chdir=terraform init terraform -chdir=terraform apply -terraform-destroy: +destroy: terraform -chdir=terraform destroy diff --git a/src/app.py b/src/app.py index 8aa1c4e..64278ee 100644 --- a/src/app.py +++ b/src/app.py @@ -1,11 +1,28 @@ -from flask import Flask +from flask import Flask, render_template +from database import get_db_connection, migrate app = Flask(__name__) + @app.route("/health") def health(): + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT 1") + cursor.fetchone() return "OK" + @app.route("/") def hello_world(): - return "

Hello, World!

" + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT * FROM users") + users = cursor.fetchall() + print(users) + return render_template("index.html", users=users) + + +if __name__ == "__main__": + migrate() + app.run(host="0.0.0.0", port=8080) diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..0c6b8f5 --- /dev/null +++ b/src/database.py @@ -0,0 +1,28 @@ +import os +import psycopg2 + +conn = None + + +def get_db_connection(): + global conn + if not conn: + conn = psycopg2.connect( + host=os.environ.get("DB_HOST", "localhost"), + database=os.environ.get("DB_NAME", "postgres"), + user=os.environ.get("DB_USER", "postgres"), + password=os.environ.get("DB_PASSWORD", "postgres"), + sslmode=os.environ.get("DB_SSLMODE", "prefer"), + ) + return conn + + +def migrate(): + conn = get_db_connection() + cur = conn.cursor() + cur.execute( + "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name VARCHAR(255))" + ) + cur.execute("INSERT INTO users (name) VALUES ('John')") + cur.execute("INSERT INTO users (name) VALUES ('Oliver')") + conn.commit() diff --git a/requirements.txt b/src/requirements.txt similarity index 73% rename from requirements.txt rename to src/requirements.txt index d40fb70..919dedd 100644 --- a/requirements.txt +++ b/src/requirements.txt @@ -1,5 +1,5 @@ blinker==1.7.0 -click==8.1.7 Flask==3.0.2 itsdangerous==2.1.2 +psycopg2-binary==2.9.9 Werkzeug==3.0.1 diff --git a/src/templates/index.html b/src/templates/index.html new file mode 100644 index 0000000..e0be914 --- /dev/null +++ b/src/templates/index.html @@ -0,0 +1,10 @@ + +Hello from Flask + +

Here are some users

+ + diff --git a/terraform/.terraform.lock.hcl b/terraform/.terraform.lock.hcl index 59a6cca..c00b5e6 100644 --- a/terraform/.terraform.lock.hcl +++ b/terraform/.terraform.lock.hcl @@ -20,3 +20,22 @@ provider "registry.terraform.io/hashicorp/azurerm" { "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } + +provider "registry.terraform.io/hashicorp/random" { + version = "3.6.0" + hashes = [ + "h1:R5Ucn26riKIEijcsiOMBR3uOAjuOMfI1x7XvH4P6B1w=", + "zh:03360ed3ecd31e8c5dac9c95fe0858be50f3e9a0d0c654b5e504109c2159287d", + "zh:1c67ac51254ba2a2bb53a25e8ae7e4d076103483f55f39b426ec55e47d1fe211", + "zh:24a17bba7f6d679538ff51b3a2f378cedadede97af8a1db7dad4fd8d6d50f829", + "zh:30ffb297ffd1633175d6545d37c2217e2cef9545a6e03946e514c59c0859b77d", + "zh:454ce4b3dbc73e6775f2f6605d45cee6e16c3872a2e66a2c97993d6e5cbd7055", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:91df0a9fab329aff2ff4cf26797592eb7a3a90b4a0c04d64ce186654e0cc6e17", + "zh:aa57384b85622a9f7bfb5d4512ca88e61f22a9cea9f30febaa4c98c68ff0dc21", + "zh:c4a3e329ba786ffb6f2b694e1fd41d413a7010f3a53c20b432325a94fa71e839", + "zh:e2699bc9116447f96c53d55f2a00570f982e6f9935038c3810603572693712d0", + "zh:e747c0fd5d7684e5bfad8aa0ca441903f15ae7a98a737ff6aca24ba223207e2c", + "zh:f1ca75f417ce490368f047b63ec09fd003711ae48487fba90b4aba2ccf71920e", + ] +} diff --git a/terraform/main.tf b/terraform/main.tf index ec8ec56..803d91d 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -6,13 +6,20 @@ resource "azurerm_resource_group" "main" { ## # Database ## +resource "random_pet" "database_username" { + separator = "" +} +resource "random_password" "database_password" { + length = 16 + special = false +} resource "azurerm_postgresql_server" "main" { name = var.name location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name - administrator_login = "psqladmin" - administrator_login_password = "H@Sh1CoR3!" + administrator_login = random_pet.database_username.id + administrator_login_password = random_password.database_password.result sku_name = "B_Gen5_1" version = "11" @@ -33,6 +40,26 @@ resource "azurerm_container_registry" "main" { sku = "Basic" } +data "azurerm_container_registry_scope_map" "main" { + name = "_repositories_pull" + resource_group_name = azurerm_resource_group.main.name + container_registry_name = azurerm_container_registry.main.name +} + +resource "azurerm_container_registry_token" "main" { + name = var.name + container_registry_name = azurerm_container_registry.main.name + resource_group_name = azurerm_resource_group.main.name + scope_map_id = data.azurerm_container_registry_scope_map.main.id +} + +resource "azurerm_container_registry_token_password" "main" { + container_registry_token_id = azurerm_container_registry_token.main.id + + password1 { + } +} + ## # Application ## @@ -56,12 +83,72 @@ resource "azurerm_container_app" "main" { resource_group_name = azurerm_resource_group.main.name revision_mode = "Single" + secret { + name = "registry-token" + value = one(azurerm_container_registry_token_password.main.password1).value + } + + registry { + server = azurerm_container_registry.main.login_server + username = azurerm_container_registry_token.main.name + password_secret_name = "registry-token" + } + + ingress { + allow_insecure_connections = false + external_enabled = true + target_port = 8080 + traffic_weight { + latest_revision = true + percentage = 100 + } + } + + secret { + name = "db-username" + value = "${random_pet.database_username.id}@${azurerm_postgresql_server.main.name}" + } + + secret { + name = "db-password" + value = random_password.database_password.result + } + template { container { name = "maincontainerapp" - image = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest" + image = var.image cpu = 0.25 memory = "0.5Gi" + + env { + name = "DB_HOST" + value = azurerm_postgresql_server.main.fqdn + } + env { + name = "DB_SSLMODE" + value = "require" + } + env { + name = "DB_USER" + secret_name = "db-username" + } + env { + name = "DB_PASSWORD" + secret_name = "db-password" + } + + liveness_probe { + path = "/health" + port = 8080 + transport = "HTTP" + } + + readiness_probe { + path = "/health" + port = 8080 + transport = "HTTP" + } } } } diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 59ebba7..6623b06 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -1,3 +1,3 @@ -output "database_fqdn" { - value = azurerm_postgresql_server.main.fqdn +output "container_registry_name" { + value = azurerm_container_registry.main.name } diff --git a/terraform/providers.tf b/terraform/providers.tf index 0c5d37d..a278201 100644 --- a/terraform/providers.tf +++ b/terraform/providers.tf @@ -1,12 +1,18 @@ terraform { required_providers { azurerm = { - source = "hashicorp/azurerm" + source = "hashicorp/azurerm" version = "3.94.0" } + random = { + source = "hashicorp/random" + version = "3.6.0" + } } } +provider "random" {} + provider "azurerm" { features {} } diff --git a/terraform/variables.tf b/terraform/variables.tf index 3be12d2..578f3f9 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -2,3 +2,9 @@ variable "name" { description = "The name of the application" type = string } + +variable "image" { + description = "The image to run" + type = string + default = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest" +}