{"id":19421422,"url":"https://github.com/saymolet/blog-flask","last_synced_at":"2026-04-11T09:03:22.503Z","repository":{"id":159446747,"uuid":"593195826","full_name":"saymolet/blog-flask","owner":"saymolet","description":"Blog build with Python using Flask. Has authentication, password hashing, database, forms, comments, admin privileges for the first user, and errorhandling. ","archived":false,"fork":false,"pushed_at":"2023-05-18T13:49:33.000Z","size":7243,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-02-25T04:15:21.649Z","etag":null,"topics":["blog","docker","flask","gcp","kubernetes","python","terraform"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/saymolet.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-01-25T13:13:52.000Z","updated_at":"2023-05-23T09:24:30.000Z","dependencies_parsed_at":null,"dependency_job_id":"b4a9007b-8a7e-4035-affc-b285268753e4","html_url":"https://github.com/saymolet/blog-flask","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/saymolet/blog-flask","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saymolet%2Fblog-flask","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saymolet%2Fblog-flask/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saymolet%2Fblog-flask/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saymolet%2Fblog-flask/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/saymolet","download_url":"https://codeload.github.com/saymolet/blog-flask/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/saymolet%2Fblog-flask/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279015049,"owners_count":26085643,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-13T02:00:06.723Z","response_time":61,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blog","docker","flask","gcp","kubernetes","python","terraform"],"created_at":"2024-11-10T13:28:07.467Z","updated_at":"2025-10-13T12:22:17.740Z","avatar_url":"https://github.com/saymolet.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Flask Blog\r\n\r\nThis blog was built with Python using Flask. Complete with authentication, password hashing, a database, forms, comments, admin privileges for the first user, and error-handling. You can either deploy it with the help of the docker-compose file or deploy it to the Kubernetes cluster with the help of the custom Helm chart.\r\n\r\n## Demo\r\n\r\n\r\nhttps://user-images.githubusercontent.com/101016860/215082267-2daccfbb-82a5-43ff-b7ab-9d2a8104b892.mp4\r\n\r\n\r\n## Prerequisites\r\n#### For Docker deployment:\r\n\r\n* [Docker](https://docs.docker.com/engine/install/ubuntu/)\r\n* [Docker Compose](https://docs.docker.com/compose/install/linux/)\r\n\r\n#### For Kubernetes deployment:\r\n\r\n* Any kubernetes cluster (Managed or Local)\r\n* [Helm](https://helm.sh/docs/intro/install/)\r\n* [Helmfile](https://helmfile.readthedocs.io/en/latest/#installation)\r\n* [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/) \r\n\r\n#### For CI/CD Pipeline:\r\n\r\n* Kubernetes cluster (this project will use GKE)\r\n* Separate [Docker-in-docker Jenkins](https://www.jenkins.io/doc/book/installing/docker/) instance\r\n* Artifact Registry repository\r\n\r\nOn Jenkins:\r\n* [Helm](https://helm.sh/docs/intro/install/) \r\n* [Helmfile](https://helmfile.readthedocs.io/en/latest/#installation) \r\n* [Kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/)\r\n* [Cloud SDK](https://cloud.google.com/sdk/docs/install#linux) \r\n* Python3 + [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)\r\n\r\nMost of the preparation and infrastructure for the CI/CD setup is automated through Terraform.\r\n\r\nIn both setups, you need to export the following environmental variables (For CI/CD, you need fill out credentials in Jenkins)\r\n\r\nExport database password as an environmental variable:\r\n```shell\r\n$ export DB_PASSWORD={your_password}\r\n```\r\n\r\nExport pgadmin4 password as an environmental variable:\r\n```shell\r\n$ export PG4_PASSWORD={your_password}\r\n```\r\n\r\nExport pgadmin4 email as an environmental variable:\r\n```shell\r\n$ export PG4_EMAIL={your_email}\r\n```\r\n\r\nExport forms secret key as an environmental variable:\r\n```shell\r\n$ export FORMS_KEY={random_string}\r\n```\r\n\r\nIn addition to that, if you want to deploy it to Kubernetes, you need to export a couple more environment variables to be able to pull an image locally. The project was tailored for a pipeline, which pulls from a private Docker repository, but you can easily put a plain public image there. The syntaxis for an image name in a helmfile is `DOCKER_REPO/IMAGE_NAME:IMAGE_VERSION`. \r\n\r\nExport the docker repository,  image name, and image version:\r\n```shell\r\n$ export DOCKER_REPO={your_private_repo} or {docker_hub_account}\r\n$ export IMAGE_NAME={the_name_of_the_image}\r\n$ export IMAGE_VERSION={image_version}\r\n```\r\n\r\nIf you want to test it locally you can pull my public image `saymolet/flask-blog:3`\r\n```shell\r\n$ export DOCKER_REPO=saymolet\r\n$ export IMAGE_NAME=flask-blog\r\n$ export IMAGE_VERSION=3\r\n```\r\n\r\n## Usage\r\n\r\n### Docker\r\n\r\nBuild docker image of the application:\r\n\r\n```shell\r\n$ docker build -t flask-blog .\r\n```\r\n\r\nThen use the docker-compose file to bring up three containers:\r\n\r\n```shell\r\n$ docker-compose -f docker-compose.yaml up\r\n```\r\n\r\nThe application will be available at `127.0.0.1:80`. The first user to register is granted admin privileges to create, edit, and delete posts from the blog. Other users can only read and comment on posts.\r\n\r\nYou can access pgadmin4 at `127.0.0.1:8080`. The email for admin user is env variable `PG4_EMAIL`. The password is the environment variable `PG4_PASSWORD` exported at the start.\r\n\r\nThe first time you log in to pgadmin4 you will not see the database just yet. Import the server by clicking `Tools--\u003eImport/Export Servers...--\u003eUpload the servers-docker.json file located in servers-jsons folder--\u003eNext--\u003eChoose the server--\u003eNext--\u003eFinish` Now just expand the `Servers` tab and input the password from `DB_PASSWORD` env var.\r\n\r\n\u003cdiv  align=\"center\"\u003e\r\n\u003cimg  src=\"images/img.png\"  alt=\"drawing\"  width=\"700\"/\u003e\r\n\u003c/div\u003e\r\n\r\nTo see the data go to `Servers--\u003e{your_db_name}--\u003eposts--\u003eSchemas--\u003epublic--\u003eTables`. Then right click on any table and select `View/Edit Data--\u003eAll Rows`.\r\n\r\nUninstall docker setup with the folowing command:\r\n```shell\r\ndocker-compose -f docker-compose.yaml down\r\n```\r\n\r\n### Kubernetes\r\n\r\nI've written a custom helm chart for this project. This chart will deploy Stateful Sets for pgadmin4 and PostgreSQL, Deployment for the application, and all the respected Config Maps, Secrets, Services and VolumeClaims. This chart was tested locally with the help of the `minikube`, and all worked as expected. Also, I've tested the chart on a managed GKE cluster (Google Cloud).\r\n\r\nDeploy the helm chart:\r\n```shell\r\n$ helmfile sync\r\n```\r\n\r\nAfter a short while, you will see three pods:\r\n```\r\n$ kubectl get pods\r\nNAME                          READY   STATUS    RESTARTS   AGE\r\nflask-blog-8454db7dcc-nmh57   1/1     Running   0          12h\r\npgadmin-0                     1/1     Running   0          12h\r\npostgres-0                    1/1     Running   0          12h\r\n```\r\n\r\nA couple of services will be created. For example:\r\n\r\n```\r\n$ kubectl get svc\r\nNAME                 TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)          AGE\r\nflask-blog-service   LoadBalancer   10.108.14.197   34.159.197.191   5000:30440/TCP   12h\r\npgadmin-service      NodePort       10.108.8.100    \u003cnone\u003e           5050:30419/TCP   12h\r\npostgres-service     ClusterIP      10.108.13.150   \u003cnone\u003e           5432/TCP         12h\r\n```\r\nIf you are using minikube as your cluster you need to forward services using [minikube service](https://minikube.sigs.k8s.io/docs/commands/service/)\r\n\r\nIf you see this, then everything is fine. The application itself is reached through the LoadBalancer. In this example, you can reach the app by following `34.159.197.191`. \r\nThe pgadmin4 can be reached through a NodePort, so you need to do some port-forwarding.\r\n```\r\nkubectl port-forward pgadmin-service 8081:5050\r\n```\r\nor\r\n```\r\nkubectl port-forward $(kubectl get pod --selector=\"app=pgadmin\" --output jsonpath='{.items[0].metadata.name}') 8081:5050\r\n```\r\n\u003eTo see the DB, you just need to import the server for pgadmin exactly the same as in the Docker deployment; just use the `servers-k8s.json` file instead.\r\n\r\nDestroy and purge the deployed helm charts:\r\n```shell\r\n$ helmfile destroy\r\n```\r\n\u003eNote that the command above WILL NOT delete the PVC and PV from the cluster. You need to delete them manually\r\n\r\n### CI/CD Pipeline with Jenkins\r\n\r\n\u003cdiv  align=\"center\"\u003e\r\n\u003cimg  src=\"images/cicd.svg\"  alt=\"ci_cd\"/\u003e\r\n\u003c/div\u003e\r\n\r\nTo properly use the pipeline with Jenkins, you need to fork this repository. Jenkins will pull from your repository and commit back to your repository. After deployment, `flask-blog` pods will have a `NodePort` service attached. This is convenient if you want to further setup an IngressController with your custom domain. After that, you can easily issue an SSL certificate by following the [Using Google-managed SSL certificates](https://cloud.google.com/kubernetes-engine/docs/how-to/managed-certs) instructions. But if you want to demo the application, you can either configure port-forwarding for the deployed `NodePort` service, or change line `36` in `values/flask-blog-values.yaml` from\r\n\r\n```\r\n##### SERVICE #####\r\nserviceName: flask-blog-service\r\nserviceType: NodePort\r\n```\r\nto:\r\n```\r\n##### SERVICE #####\r\nserviceName: flask-blog-service\r\nserviceType: LoadBalancer\r\n```\r\n\r\nAfter this, you can redeploy (or deploy) the application, and you will have an HTTP LoadBalancer attached to your cluster.\r\n\r\nThis pipeline was tailored for Google Kubernetes Engine (GKE) on Google Cloud Platform (GCP). Most of the preparation is automated through Terraform. `cd` into `terraform` directory and login to your GCP account using `gcloud auth application-default login` command. After that, execute `terraform plan` and `terraform apply` specifying the id of the project you want to deploy to. Terraform needs around `15-20` minutes to bring up the infrastructure. Jenkins VM has a startup script that will install all the necessary tools for the pipeline. After the script finishes, it will attach the initialAdminPassword to the VM as custom metadata called `ADMIN_PASS`. Pluck it into Jenkins and delete it afterwards.\r\n\r\nTerraform will bring up the following:\r\n* Static IP for Jenkins VM\r\n* Jenkins VM\r\n\t* e2-standard-2\r\n\t* ubuntu 20.04\r\n\t* Custom metadata\r\n* Service account for Jenkins VM \r\n\t* kubernetesEngineDeveloper role\r\n\t* custom_computeMetadataWriter role (compute.instances.get, compute.instances.list, compute.instances.setMetadata)\r\n\t* IAM binding to Artifact Registry\r\n* GKE Node Pool\r\n\t* autoscaling (1-4 nodes)\r\n\t* e2-medium\r\n* GKE Cluster\r\n* Service account for GKE Nodes\r\n\t* artifactRegistryReader role\r\n* Artifact Registry repository\r\n\r\nThe pipeline has four stages.\r\n* Version Increment\r\n* Containerize\r\n* Deploy to Production\r\n* Version Control\r\n\r\n#### Version Increment\r\nThe app's version is incremented using a custom script located in the `/scripts` directory. Saves the image name, image version, and build number as an environmental variable. Also pulls sensitive data from Jenkins credentials and exports them as environmental variables to use with helmfile.\r\n\r\n#### Containerize\r\nBuild the image with the specified Docker repository server, image name, image version, and build number. [Logins to the private docker repo](https://cloud.google.com/artifact-registry/docs/docker/authentication#token) using the Jenkins service account with the appropriate IAM role attached to the instance and pushes the image.\r\n\r\n#### Deploy to Production\r\nDeploys the application to GKE using `helmfile` command. The nodes are able to pull the image from the private Artifact Registry repository through a service account with the right IAM roles.\r\n\r\n#### Version Control\r\nLogin, configure, and push the version bump to the main branch. This is done with fine grained GitHub tokens, so you need to put yours inside Jenkins credentials.\r\n\r\nYou need to change the default passwords in Jenkins credentials so it works right. After that, you can configure a pipeline to pull from your forked repo!\r\n\r\n### If you have any questions or propositions please contact me at [vlad@samoilenko.xyz](mailto:vlad@samoilenko.xyz). I will gladly answer them.\r\n\r\n## Reference\r\n\r\nCSS and the idea for this application came from [Angela Yu](https://github.com/angelabauer)\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaymolet%2Fblog-flask","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsaymolet%2Fblog-flask","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsaymolet%2Fblog-flask/lists"}