diff --git a/.env.example.local b/.env.example.local index 5f6d523..272ae3d 100644 --- a/.env.example.local +++ b/.env.example.local @@ -52,7 +52,11 @@ GOOD_APP_HEALTH_CHECK_PATTERN=xxx # This is for environment variables for docker-compose-app-${app_env}. DOCKER_COMPOSE_ENVIRONMENT={"XDEBUG_CONFIG":"idekey=IDE_DEBUG","PHP_IDE_CONFIG":"serverName=laravel-crud-boilerplate"} DOCKER_BUILD_ARGS={} +DOCKER_BUILD_LABELS=[] +DOCKER_BUILD_SHA_INSERT_GIT_ROOT= + DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=[] +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M diff --git a/.env.example.real b/.env.example.real index 606ea01..f7194a7 100644 --- a/.env.example.real +++ b/.env.example.real @@ -54,11 +54,15 @@ GOOD_APP_HEALTH_CHECK_PATTERN=xxx # This is for environment variables for docker-compose-app-${app_env}. DOCKER_COMPOSE_ENVIRONMENT={} DOCKER_BUILD_ARGS={} +DOCKER_BUILD_LABELS=[] +DOCKER_BUILD_SHA_INSERT_GIT_ROOT= + # This is overwritten on docker-compose-${project_name}-${app_env}.yml DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES=[] # This is added on docker-compose-${project_name}-nginx.yml DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=["./shared/nginx-error-logs:/var/log/nginx"] - +# Check if the host folder or file exists +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M diff --git a/.env.java.local b/.env.java.local index 00a48f4..3889b6d 100644 --- a/.env.java.local +++ b/.env.java.local @@ -47,7 +47,11 @@ DOCKER_COMPOSE_ENVIRONMENT={"TZ":"Asia/Seoul"} # This goes with "docker build ... in the 'run.sh' script file", and the command always contain "HOST_IP" and "APP_ENV" above. # docker exec -it CONTAINER_NAME cat /var/log/env_build_args.log DOCKER_BUILD_ARGS={"DOCKER_BUILDKIT":"1","PROJECT_ROOT_IN_CONTAINER":"/var/www/server/spring-sample-h-auth","APP_ENV":"local"} +DOCKER_BUILD_LABELS=[] +DOCKER_BUILD_SHA_INSERT_GIT_ROOT= + DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=[] +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M diff --git a/.env.java.real b/.env.java.real index 01ebaf5..cbbd7ef 100644 --- a/.env.java.real +++ b/.env.java.real @@ -48,8 +48,12 @@ DOCKER_COMPOSE_ENVIRONMENT={"TZ":"Asia/Seoul"} # This goes with "docker build ... in the 'run.sh' script file", and the command always contain "HOST_IP" and "APP_ENV" above. # docker exec -it CONTAINER_NAME cat /var/log/env_build_args.log DOCKER_BUILD_ARGS={"DOCKER_BUILDKIT":"1","PROJECT_ROOT_IN_CONTAINER":"/var/www/server/spring-sample-h-auth","APP_ENV":"production"} +DOCKER_BUILD_LABELS=["foo=happy","bar=sad"] +DOCKER_BUILD_SHA_INSERT_GIT_ROOT= + DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES=["./samples/spring-sample-h-auth/logs:/var/www/files"] DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=["./shared/nginx-error-logs:/var/log/nginx"] +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M diff --git a/.env.java.real.commercial.ssl.sample b/.env.java.real.commercial.ssl.sample index b6e9d1a..870fe9b 100644 --- a/.env.java.real.commercial.ssl.sample +++ b/.env.java.real.commercial.ssl.sample @@ -47,11 +47,13 @@ DOCKER_COMPOSE_ENVIRONMENT={"TZ":"Asia/Seoul"} # This goes with "docker build ... in the 'run.sh' script file", and the command always contain "HOST_IP" and "APP_ENV" above. # 2) ''/var/web/project/spring-sample-h-auth' is here as well DOCKER_BUILD_ARGS={"DOCKER_BUILDKIT":"1","PROJECT_ROOT_IN_CONTAINER":"/var/www/server/spring-sample-h-auth","FILE_STORAGE_ROOT_IN_CONTAINER":"/var/www/files","JVM_XMS":"2048","JVM_XMX":"4096"} +DOCKER_BUILD_LABELS=["foo.mylabel=happy","bar.mylabel=happy"] +DOCKER_BUILD_SHA_INSERT_GIT_ROOT=/var/www/server/spring-sample-h-auth # 3) ''/var/web/project/spring-sample-h-auth' is here as well. The thing is you should locate 'application.properties', 'logback-spring.xml', 'yourdomain.com.jks' on the './src/main/resource' folder. DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES=["/var/web/files/spring-sample-h-auth:/var/www/files","/var/web/project/spring-sample-h-auth/src/main/resources:/var/www/server/spring-sample-h-auth/src/main/resources"] DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=["/var/web/files/nginx/spring-sample-h-auth/logs:/var/log/nginx"] - +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M diff --git a/.env.node.local b/.env.node.local index aebab9b..cb1df9d 100644 --- a/.env.node.local +++ b/.env.node.local @@ -44,7 +44,11 @@ GOOD_APP_HEALTH_CHECK_PATTERN=docs # This is for environment variables for docker-compose-app-${app_env}. DOCKER_COMPOSE_ENVIRONMENT={"MONGODB_URL":"mongodb://host.docker.internal:27017/node-boilerplate","NODE_ENV":"development"} DOCKER_BUILD_ARGS={} +DOCKER_BUILD_LABELS=["foo=happy","bar=sad"] +DOCKER_BUILD_SHA_INSERT_GIT_ROOT= + DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=[] +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M diff --git a/.env.php.local b/.env.php.local index 52a5fe4..4622937 100644 --- a/.env.php.local +++ b/.env.php.local @@ -48,8 +48,11 @@ DOCKER_COMPOSE_ENVIRONMENT={"XDEBUG_CONFIG":"idekey=IDE_DEBUG","PHP_IDE_CONFIG": # This goes with "docker build ... in the 'run.sh' script file", and the command always contain "HOST_IP" and "APP_ENV" above. # docker exec -it CONTAINER_NAME cat /var/log/env_build_args.log DOCKER_BUILD_ARGS={"SAMPLE":"YAHOO","SAMPLE2":"YAHOO2"} -DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=[] +DOCKER_BUILD_LABELS=["foo=happy","bar=sad"] +DOCKER_BUILD_SHA_INSERT_GIT_ROOT= +DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=[] +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M diff --git a/.env.php.real b/.env.php.real index c36b866..3726efa 100644 --- a/.env.php.real +++ b/.env.php.real @@ -48,10 +48,15 @@ DOCKER_COMPOSE_ENVIRONMENT={"XDEBUG_CONFIG":"idekey=IDE_DEBUG","PHP_IDE_CONFIG": # This goes with "docker build ... in the 'run.sh' script file", and the command always contain "HOST_IP" and "APP_ENV" above. # docker exec -it CONTAINER_NAME cat /var/log/env_build_args.log DOCKER_BUILD_ARGS={"SAMPLE":"YAHOO","SAMPLE2":"YAHOO2","shared_volume_group_id":"1351","shared_volume_group_name":"laravel-shared-volume-group"} +DOCKER_BUILD_LABELS=["foo=happy","bar=sad"] +# Your Git's commit SHA will be added as a label to DOCKER_BUILD_LABELS when your container is built. +DOCKER_BUILD_SHA_INSERT_GIT_ROOT= + # In the case of "REAL," the project is not synchronized in its entirety. The source codes that are required for only production are injected. # For SSL, the host folder is recommended to be './.docker/ssl' to be synchronized with 'docker-compose-nginx-original.yml' DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES=["./shared/app-error-logs:/var/www/app/storage/logs","./.docker/ssl:/etc/apache2/ssl"] DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=["./shared/nginx-error-logs:/var/log/nginx"] +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M diff --git a/.gitignore b/.gitignore index 9580455..b707aa2 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,10 @@ /shared/system-logs/* !/shared/system-logs/.gitkeep +/bin/* +!/bin/.gitkeep + + **/.idea /.env /.env.ready.* diff --git a/README.md b/README.md index fb389fe..754bc2f 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,8 @@ > One Simple Zero-Downtime Blue-Green Deployment with your Dockerfiles -Deploying web projects should be [simple, with high availability and security](https://github.com/Andrew-Kang-G/docker-blue-green-runner?tab=readme-ov-file#Quick-Guide-on-Usage). - -- Use ``the latest Release version`` OR at least ``tagged versions`` for your production, NOT the latest commit of the 'main' branch. -- In production, place your project in a separate folder, not in the samples folder, as they are just examples. - ## Table of Contents +- [Process Summary](#process-summary) - [Features](#features) - [Requirements](#requirements) - [Quick Start with Samples](#quick-start-with-samples) @@ -34,8 +30,7 @@ Deploying web projects should be [simple, with high availability and security](h - [Consul](#consul) - [USE_NGINX_RESTRICTION on .env](#use_nginx_restriction-on-env) - [Advanced](#advanced) -- [Process Summary](#process-summary) -- [Gitlab Container Registry](#gitlab-container-registry) +- [GitLab Container Registry (Production)](#gitlab-container-registry-production) - [Upload Image (CI/CD Server -> Git)](#upload-image-cicd-server---git) - [Download Image (Git -> Production Server)](#download-image-git---production-server) - [Extra Information](#extra-information) @@ -47,6 +42,44 @@ Deploying web projects should be [simple, with high availability and security](h --- +## Process Summary + +- Term Reference + - ``All`` means below is "App", "Nginx", "Consul&Registrator". + - ``(Re)Load`` means ``docker run.... OR docker-compose up``. + - ``State`` is ``Blue`` or ``Green`` + - More is on [Terms](#terms) +- Load Consul & Registrator, then the App, and finally Nginx to prevent upstream errors. + + +```mermaid +graph TD; + A[Initialize and Set Variables] --> B[Backup All Images] + B --> C[Check the .env File Integrity] + C --> D[Build All Images] + D --> E[Create Consul Network] + E --> F{Reload Consul if Required} + F -- Yes --> G[Reload Consul] + F -- No --> H[Load Your App] + G --> H[Load Your App] + H --> I[Check App Integrity] + I --> J{Reload Nginx if Required} + J -- Yes --> K[Check Nginx Template Integrity by Running a Test Container] + J -- No --> L[Check All Containers' Health] + K --> L[Check All Containers' Health] + L --> M{Set New State Using Consul Template} + M -- Fails --> O[Run Nginx Contingency Plan] + M -- Success --> N[External Integrity Check] + O --> N[External Integrity Check] + N -- Fails --> P[Rollback App if Needed] + N -- Success --> Q["Remove the Opposite State (Blue or Green) from the Running Containers"] + P --> Q["Remove the Opposite State from the Running Containers"] + Q --> R[Clean Up Dangling Images] + R --> S[Deployment Complete] + +``` +![img5.png](/documents/images/img5.png) +![img6.png](documents/images/img6.png) ## Features - **No Unpredictable Errors in Reverse Proxy and Deployment** @@ -58,6 +91,7 @@ Deploying web projects should be [simple, with high availability and security](h - **From Scratch** - Docker-Blue-Green-Runner's `run.sh` script is designed to simplify deployment: "With your `.env`, project, and a single Dockerfile, simply run 'bash run.sh'." This script covers the entire process from Dockerfile build to server deployment from scratch. + - This means you can easily migrate to another server with just the files mentioned above. - In contrast, Traefik requires the creation and gradual adjustment of various configuration files, which can introduce the types of errors mentioned above. - Focus on zero-downtime deployment on a single machine. @@ -105,10 +139,10 @@ Deploying web projects should be [simple, with high availability and security](h | git | N/A | Manual | - | | bash | 4.4 at least | Manual | - | | curl | N/A | Manual | - | -| yq | 4.35.1 | Manual | Use v4.35.1 instead of the latest version. The lastest version causes a parsing error | +| yq | 4.35.1 | Auto | Use v4.35.1 instead of the latest version. The lastest version causes a parsing error | | consul (docker image) | 1.14.11 | Auto | An error occurred due to a payload format issue while the lastest version of it was communicating with gliderlabs/registrator. | | gliderlabs/registrator (docker image) | master | Auto | | -| nginx (docker image) | latest | Auto | Considering changing it to a certain version, but until now no issues have been detected. | +| nginx (docker image) | 1.25.4 | Auto | Considering changing it to a certain version, but until now no issues have been detected. | | docker | 24~27 | Manual | I think too old versions could cause problems, and the lastest version v27.x causes only a warning message. | | docker-compose | 2 | Manual | I think too old versions could cause problems, and the v2 is recommended. | @@ -273,9 +307,9 @@ sudo bash run.sh - ```shell APP_URL=http://localhost:<--!host-port-number!--> PROJECT_PORT=<--!common-port-number!--> OR - PROJECT_PORT=[<--!host-port-number!-->,<--!new-project-port-number!-->] + PROJECT_PORT=[<--!host-port-number!-->,<--!internal-project-port-number!-->] ``` - - Additionally, the APP_URL parameter is used for 'check_availability_out_of_container' at [Structure](#Structure) + - Additionally, the `APP_URL` parameter is used for the ["External Integrity Check"](#process-summary) process. - You can set it as https://localhost:13000 or https://your-domain:13000 for production environments. (Both configurations are acceptable) - Moreover, the Runner parses the protocol (http or https), and if it is https, it checks for SSL certificates in the .docker/ssl directory on the host according to the following settings: - ```shell @@ -288,9 +322,23 @@ sudo bash run.sh # Set this to 'real' in the .env file for production environments. APP_ENV=real +# This path is used for both internal and external health checks. +# Note: Do not include a leading slash ("/") at the start of the path. +# Example: "api/v1/health" (correct), "/api/v1/health" (incorrect) +APP_HEALTH_CHECK_PATH=api/v1/health + +# The BAD & GOOD conditions are checked using an "AND" condition. +# To ignore the "BAD_APP_HEALTH_CHECK_PATTERN", set it to a non-existing value (e.g., "###lsdladc"). +BAD_APP_HEALTH_CHECK_PATTERN=DOWN + +# Pattern required for a successful health check. +GOOD_APP_HEALTH_CHECK_PATTERN=UP + + # The 'real' setting requires defining 'DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES'. DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES=["/my-host/files/:/in-container/files", "/my-host/java-spring-project/src/main/resources:/in-container/java-spring-project/src/main/resources"] - +# Check if the host folder or file exists +DOCKER_COMPOSE_HOST_VOLUME_CHECK=false # If APP_ENV is set to 'local', as specified in 'docker-compose-app-local-original.yml', synchronize your entire project as follows: "HOST_ROOT_LOCATION:PROJECT_LOCATION". # [IMPORTANT] If this is set to true, Nginx will be restarted, resulting in a short downtime. # This option should be used when upgrading the Runner. See the "Upgrade" section below. @@ -299,6 +347,10 @@ NGINX_RESTART=false # Setting this to 'true' is not recommended for normal operation as it results in prolonged downtime. CONSUL_RESTART=false +# Specify the location of the .git folder for your project here to enable tracking through container labels. +# To track, simply run `bash check-current_states.sh`. +DOCKER_BUILD_SHA_INSERT_GIT_ROOT= + # Not recommended for normal operation as it leads to a long downtime. # If this is set to true, it entails running 'stop-all-containers.sh & remove-all-images.sh'. # In case your project is renamed or moved to another folder, Docker may not work properly. @@ -402,6 +454,8 @@ bash check-current-states.sh [DEBUG] ! Checked which (Blue OR Green) is currently running... (Final Check) : blue_score : 130, green_score : 27, state : blue, new_state : green, state_for_emergency : blue, new_upstream : https://PROJECT_NAME:8300. ``` - The higher the score a state receives, the more likely it is to be the currently running state. So the updated App should be deployed as the non-occupied state(new_state). + +- ![img6.png](documents/images/img6.png) ### Emergency - Nginx (like when Nginx is NOT booted OR 502 error...) @@ -491,44 +545,9 @@ bash check-source-integrity.sh - **For the properties of 'environment, volumes', use .env instead of setting them on the yml.** - Set ```USE_MY_OWN_APP_YML=true``` on .env - ```bash run.sh``` - -## Process Summary - -- Term Reference - - ``All`` means below is "App", "Nginx", "Consul&Registrator". - - ``(Re)Load`` means ``docker run.... OR docker-compose up``. - - ``State`` is ``Blue`` or ``Green`` - - More is on [Terms](#terms) -- Load Consul & Registrator, then the App, and finally Nginx to prevent upstream errors. -```mermaid -graph TD; - A[Initialize and Set Variables] --> B[Backup All Images] - B --> C[Check the .env File Integrity] - C --> D[Build All Images] - D --> E[Create Consul Network] - E --> F{Reload Consul if Required} - F -- Yes --> G[Reload Consul] - F -- No --> H[Load Your App] - G --> H[Load Your App] - H --> I[Check App Integrity] - I --> J{Reload Nginx if Required} - J -- Yes --> K[Check Nginx Template Integrity by Running a Test Container] - J -- No --> L[Check All Containers' Health] - K --> L[Check All Containers' Health] - L --> M{Set New State Using Consul Template} - M -- Fails --> O[Run Nginx Contingency Plan] - M -- Success --> N[External Integrity Check] - O --> N[External Integrity Check] - N -- Fails --> P[Rollback App if Needed] - N -- Success --> Q["Remove the Opposite State (Blue or Green) from the Running Containers"] - P --> Q["Remove the Opposite State from the Running Containers"] - Q --> R[Clean Up Dangling Images] - R --> S[Deployment Complete] - -``` -## Gitlab Container Registry +## Gitlab Container Registry (Production) ### Upload Image (CI/CD Server -> Git) - In case you run the command ``push-to-git.sh``, ``docker-blue-green-runner`` pushes one of ``Blue or Green`` images which is currently running to the address above of the Gitlab Container Registry. @@ -593,4 +612,4 @@ git status # If any changes are detected, the source code may be corrupted. docker swarm init sudo bash run.sh ``` ---- \ No newline at end of file +--- diff --git a/apply-security.sh b/apply-security.sh index fa4f512..9227703 100644 --- a/apply-security.sh +++ b/apply-security.sh @@ -2,9 +2,11 @@ set -eu source use-common.sh + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist cache_global_vars @@ -32,7 +34,24 @@ sudo chmod 750 *.sh || echo "[WARN] Running chmod 750 *.sh failed." sudo chmod 770 *.yml || echo "[WARN] Running chmod 770 *.yml failed." sudo chmod 740 .env.* || echo "[WARN] Running chmod 740 .env.* failed." sudo chmod 740 .env || echo "[WARN] Running chmod 740 .env failed." +sudo chmod -R 750 bin || echo "[WARN] Running chmod 750 for the bin folder" sudo chmod 770 .gitignore || echo "[WARN] Running chmod 770 .gitignore failed." sudo chmod -R 770 .docker/ || echo "[WARN] Running chmod -R 770 .docker/ failed." -sudo chown -R 0:${shared_volume_group_id} .docker/ || echo "[WARN] Running chgrp ${shared_volume_group_id} .docker/ failed." -set_safe_filemode_on_app +# Check if the OS is not Darwin (macOS) before running the command +if [[ "$(uname)" != "Darwin" ]]; then + sudo chown -R 0:${shared_volume_group_id} .docker/ || echo "[WARN] Running chgrp ${shared_volume_group_id} .docker/ failed." +else + echo "[NOTICE] Skipping chown command on Darwin (macOS) platform. See the README." +fi + +if [[ "$(uname)" != "Darwin" ]]; then + sudo chown -R 0:${shared_volume_group_id} bin/ || echo "[WARN] Running chgrp ${shared_volume_group_id} bin/ failed." +else + echo "[NOTICE] Skipping chown command on Darwin (macOS) platform. See the README." +fi + +if [[ "$(uname)" != "Darwin" ]]; then + set_safe_filemode_on_app +else + echo "[NOTICE] Skipping chown command on Darwin (macOS) platform. See the README." +fi diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/check-current-states.sh b/check-current-states.sh index 0f6d7fc..73ac601 100644 --- a/check-current-states.sh +++ b/check-current-states.sh @@ -2,10 +2,26 @@ set -eu source use-common.sh + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist +# Load global variables +cache_global_vars + +# Define container name +container_name="${project_name}-${state}" + +echo "[NOTICE] Project Name: ${project_name}" +display_emphasized_message "[NOTICE] Current State: ${state}" +display_emphasized_message "[NOTICE] Container Name: ${container_name}" + + +# Call the function +display_emphasized_message "$(print_git_sha_and_message "$container_name" "$docker_build_sha_insert_git_root")" -cache_global_vars \ No newline at end of file +# echo "[NOTICE] All labels inside the Container $container_name" +# docker inspect -f '{{json .Config.Labels}}' "$container_name" 2>/dev/null | yq -P \ No newline at end of file diff --git a/check-source-integrity.sh b/check-source-integrity.sh index fa33108..8c84132 100644 --- a/check-source-integrity.sh +++ b/check-source-integrity.sh @@ -2,9 +2,11 @@ set -eu source use-common.sh + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist git config apply.whitespace nowarn diff --git a/documents/Deploy-React-Project-with-DBGR.md b/documents/Deploy-React-Project-with-DBGR.md index 3c8a967..d961f96 100644 --- a/documents/Deploy-React-Project-with-DBGR.md +++ b/documents/Deploy-React-Project-with-DBGR.md @@ -17,14 +17,26 @@ - ``REDIRECT_HTTPS_TO_HTTP`` - .env file - ```dotenv + # Leave as it is HOST_IP=host.docker.internal + # Set this to 'real' in the .env file for production environments. + # This affects 'DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES' below + # If this is set to 'local', the whole projects is set to be Volumed. APP_ENV=real # It is recommended to enter your formal URL or IP, such as https://test.com, for the 'check_availability_out_of_container' test in the 'run.sh' script. # Both https://your-app.com:443 and https://localhost:443 are valid + # Docker-Blue-Runner recognizes if your App requires SSL in the Nginx router if this starts with 'https'. + # This URL is used for the "External Integrity Check" process. APP_URL=https://localhost:443 - USE_COMMERCIAL_SSL=false + # APP_URL=http://localhost:<--!host-port-number!--> + # PROJECT_PORT=<--!common-port-number!--> OR + # PROJECT_PORT=[<--!host-port-number!-->,<--!internal-project-port-number!-->] + PROJECT_PORT=[443,8360] + # In case USE_COMMERCIAL_SSL is 'false', the Runner generates self-signed SSL certificates. However, you should set any name for ``COMMERCIAL_SSL_NAME``. + # In case it is 'true', locate your commercial SSLs in the folder docker-blue-green-runner/.docker/ssl. See the comments in the .env above. + USE_COMMERCIAL_SSL=true # Your domain name is recommended. The files 'your-app.com.key', 'your-app.com.crt', and 'your-app.com.chained.crt' should be in place. COMMERCIAL_SSL_NAME=your-app.com @@ -59,23 +71,35 @@ DOCKER_FILE_LOCATION=/var/projects/your-app # This is for integrating health checkers such as "https://www.baeldung.com/spring-boot-actuators" - APP_HEALTH_CHECK_PATH=login - BAD_APP_HEALTH_CHECK_PATTERN=xxxxxxx - GOOD_APP_HEALTH_CHECK_PATTERN=Head + # This path is used for both internal and external health checks. + # Note: Do not include a leading slash ("/") at the start of the path. + # Example: "api/v1/health" (correct), "/api/v1/health" (incorrect) + APP_HEALTH_CHECK_PATH=api/v1/health + + # The BAD & GOOD conditions are checked using an "AND" condition. + # To ignore the "BAD_APP_HEALTH_CHECK_PATTERN", set it to a non-existing value (e.g., "###lsdladc"). + BAD_APP_HEALTH_CHECK_PATTERN=DOWN + + # Pattern required for a successful health check. + GOOD_APP_HEALTH_CHECK_PATTERN=UP + + # The following trick is just for skipping the check. + # APP_HEALTH_CHECK_PATH=login + # BAD_APP_HEALTH_CHECK_PATTERN=xxxxxxx + # GOOD_APP_HEALTH_CHECK_PATTERN=Head # This is for environment variables for docker-compose-app-${app_env}. DOCKER_COMPOSE_ENVIRONMENT={"TZ":"Asia/Seoul"} - # This goes with "docker build ... in the 'run.sh' script file", and the command always contain "HOST_IP" and "APP_ENV" above. - # docker exec -it CONTAINER_NAME cat /var/log/env_build_args.log - # The name "PROJECT_ROOT_IN_CONTAINER" is simply a convention used with your Dockerfile. You can change it if desired. + # [IMPORTANT] You can pass any variable to Step 2 of your Dockerfile using DOCKER_BUILD_ARGS, e.g., DOCKER_BUILD_ARGS={"PROJECT_ROOT_IN_CONTAINER":"/app"}." DOCKER_BUILD_ARGS={"PROJECT_ROOT_IN_CONTAINER":"/app"} # In the case of "REAL," the project is not synchronized in its entirety. The source codes that are required for only production are injected. - # For SSL, the host folder is recommended to be './.docker/ssl' to be synchronized with 'docker-compose-nginx-original.yml' + # For SSL, the host folder is recommended to be './.docker/ssl' to be synchronized with 'docker-compose-nginx-original.yml'. # [IMPORTANT] Run mkdir -p /var/projects/files/your-app/logs on your host machine DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES=["/var/projects/your-app/.docker/nginx/app.conf.ctmpl:/etc/nginx-template/app.conf.ctmpl","/var/projects/files/your-app/logs:/var/log/nginx"] # [IMPORTANT] Run mkdir -p /var/projects/files/nginx/logs on your host machine DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES=["/var/projects/files/nginx/logs:/var/log/nginx"] + DOCKER_COMPOSE_HOST_VOLUME_CHECK=false NGINX_CLIENT_MAX_BODY_SIZE=50M @@ -83,6 +107,7 @@ SKIP_BUILDING_APP_IMAGE=false + # Docker-Swarm(stack) is currently a beta version. Use 'compose'. ORCHESTRATION_TYPE=compose ONLY_BUILDING_APP_IMAGE=false @@ -93,7 +118,7 @@ # ex. /docs/api-app.html NGINX_RESTRICTED_LOCATION=xxx - # If you set this to 'true', you won't need to configure SSL for your app. For instance, in a Spring Boot project, you won't have to create a ".jks" file. However, in rare situations, such as when it's crucial to secure all communication lines with SSL or when converting HTTPS to HTTP causes 'curl' errors, you might need to set it to 'false'.If you set this to 'true', you don't need to set SSL on your App like for example, for a Spring Boot project, you won't need to create the ".jks" file. However, in rare cases, such as ensuring all communication lines are SSL-protected, or when HTTPS to HTTP causes 'curl' errors, you might need to set it to 'false'. + # If you set this to 'true', you won't need to configure SSL for your app. For instance, in a Spring Boot project, you won't have to create a ".jks" file. However, in rare situations, such as when it's crucial to secure all communication lines with SSL or when converting HTTPS to HTTP causes 'curl' errors, you might need to set it to 'false'.If you set this to 'true', you don't need to set SSL on your App like for example, for a Spring Boot project, you won't need to create the ".jks" file. However, in rare cases, such as ensuring all communication lines are SSL-protected, or when HTTPS to HTTP causes 'curl' errors, you might need to set it to 'false'. # 1) true : [Request]--> https (external network) -->Nginx--> http (internal network) --> App # 2) false :[Request]--> https (external network) -->Nginx--> httpS (internal network) --> App # !!! [IMPORTANT] As your App container below is Http, this should be set to 'true'. @@ -102,7 +127,7 @@ NGINX_LOGROTATE_FILE_NUMBER=7 NGINX_LOGROTATE_FILE_SIZE=1M - # You can change the values below. These settings for security related to ``set-safe-permissions.sh`` at the root of Docker-Blue-Green-Runner. + # You can change the values below. These settings for security related to ``apply-security.sh`` at the root of Docker-Blue-Green-Runner. SHARED_VOLUME_GROUP_ID=1559 SHARED_VOLUME_GROUP_NAME=mba-shared-volume-group UIDS_BELONGING_TO_SHARED_VOLUME_GROUP_ID=1000,1001 diff --git a/documents/images/img5.png b/documents/images/img5.png new file mode 100644 index 0000000..a47b17d Binary files /dev/null and b/documents/images/img5.png differ diff --git a/documents/images/img6.png b/documents/images/img6.png new file mode 100644 index 0000000..eac609a Binary files /dev/null and b/documents/images/img6.png differ diff --git a/emergency-consul-down-and-up.sh b/emergency-consul-down-and-up.sh index 935ccc5..88e55ee 100644 --- a/emergency-consul-down-and-up.sh +++ b/emergency-consul-down-and-up.sh @@ -2,9 +2,11 @@ set -eu source use-common.sh + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist diff --git a/emergency-nginx-down-and-up.sh b/emergency-nginx-down-and-up.sh index 30b7476..995363a 100644 --- a/emergency-nginx-down-and-up.sh +++ b/emergency-nginx-down-and-up.sh @@ -6,6 +6,7 @@ source use-common.sh check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist echo "[NOTICE] Substituting CRLF with LF to prevent possible CRLF errors..." diff --git a/emergency-nginx-restart.sh b/emergency-nginx-restart.sh index a1abf35..e28f26e 100644 --- a/emergency-nginx-restart.sh +++ b/emergency-nginx-restart.sh @@ -2,9 +2,11 @@ set -eu source use-common.sh + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist echo "[NOTICE] Substituting CRLF with LF to prevent possible CRLF errors..." diff --git a/remove-all-images.sh b/remove-all-images.sh index f03af6a..a9d947a 100644 --- a/remove-all-images.sh +++ b/remove-all-images.sh @@ -2,9 +2,11 @@ set -eu source use-common.sh + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist git config apply.whitespace nowarn diff --git a/rollback.sh b/rollback.sh index 0c013b4..46676fd 100644 --- a/rollback.sh +++ b/rollback.sh @@ -2,9 +2,11 @@ set -eu source use-common.sh + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist sudo sed -i -e "s/\r$//g" $(basename $0) diff --git a/run.sh b/run.sh index d1ada8f..be4a8ba 100644 --- a/run.sh +++ b/run.sh @@ -4,12 +4,15 @@ set -eu source use-common.sh + +display_checkpoint_message "Checking versions for supporting libraries...(1%)" + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist -check_yq_installed sudo chmod a+x *.sh @@ -18,7 +21,7 @@ sudo bash prevent-crlf.sh git config apply.whitespace nowarn git config core.filemode false -sleep 3 +sleep 1 source ./use-app.sh source ./use-nginx.sh @@ -181,16 +184,25 @@ backup_to_new_images(){ _main() { - # [A] Get mandatory variables + display_checkpoint_message "Initializing mandatory variables... (2%)" + cache_global_vars # The 'cache_all_states' in 'cache_global_vars' function decides which state should be deployed. If this is called later at a point in this script, states could differ. local initially_cached_old_state=${state} check_env_integrity - echo "[NOTICE] Finally, !! Deploy the App as !! ${new_state} !!, we will now deploy '${project_name}' in a way of 'Blue-Green'" + display_checkpoint_message "Deployment target between Blue and Green has been decided... (3%)" + display_planned_transition "$initially_cached_old_state" "$new_state" + sleep 2 + + if [[ "${git_image_load_from}" == "build" && -n "${project_git_sha}" && -n "${docker_build_sha_insert_git_root}" ]]; then + commit_message=$(get_commit_message "$project_git_sha" "$docker_build_sha_insert_git_root") + display_checkpoint_message "Will build this GIT version: $project_git_sha : $commit_message" + sleep 1 + fi - # [A-1] Set mandatory files ## App + display_checkpoint_message "Setting up the app configuration 'yml' for orchestration type: ${orchestration_type}... (6%)" initiate_docker_compose_file apply_env_service_name_onto_app_yaml apply_docker_compose_environment_onto_app_yaml @@ -200,8 +212,13 @@ _main() { if [[ ${skip_building_app_image} != 'true' ]]; then backup_app_to_previous_images fi + + ## Nginx if [[ ${nginx_restart} == 'true' ]]; then + + display_checkpoint_message "Since 'nginx_restart' is set to 'true', configuring the Nginx 'yml' for orchestration type: ${orchestration_type}... (7%)" + initiate_nginx_docker_compose_file apply_env_service_name_onto_nginx_yaml apply_ports_onto_nginx_yaml @@ -216,7 +233,9 @@ _main() { fi - # [A-2] Set 'Shared Volume Group' + display_checkpoint_message "Performing additional steps before building images... (10%)" + + # Set 'Shared Volume Group' # Detect the platform (Linux or Mac) if [[ "$(uname)" == "Darwin" ]]; then echo "[NOTICE] Running on Mac. Skipping 'add_host_users_to_host_group' as dscl is used for user and group management." @@ -227,7 +246,7 @@ _main() { fi fi - # [A-3] Etc. + # Etc. if [[ ${app_env} == 'local' ]]; then give_host_group_id_full_permissions fi @@ -235,19 +254,24 @@ _main() { terminate_whole_system fi - # [B] Build Docker images for the App, Nginx, Consul + + if [[ ${skip_building_app_image} != 'true' ]]; then - load_app_docker_image + display_checkpoint_message "Building Docker image for the app... ('skip_building_app_image' is set to false) (12%)" + load_app_docker_image fi - if [[ ${consul_restart} == 'true' ]]; then - load_consul_docker_image + display_checkpoint_message "Building Docker image for Consul... ('consul_restart' is set to true) (14%)" + load_consul_docker_image fi + if [[ ${nginx_restart} == 'true' ]]; then - load_nginx_docker_image + display_checkpoint_message "Building Docker image for Nginx... ('nginx_restart' is set to true) (16%)" + load_nginx_docker_image fi + if [[ ${only_building_app_image} == 'true' ]]; then echo "[NOTICE] Successfully built the App image : ${new_state}" && exit 0 fi @@ -259,17 +283,21 @@ _main() { (echo "[ERROR] Just checked all states shortly after the Docker Images had been done built. The state the App was supposed to be deployed as has been changed. (Original : ${cached_new_state}, New : ${new_state}). For the safety, we exit..." && exit 1) fi - # [C] docker-compose up the App, Nginx, Consul & * Internal Integrity Check for the App + # docker-compose up the App, Nginx, Consul & * Internal Integrity Check for the App + display_checkpoint_message "Starting docker-compose for App, Nginx, and Consul, followed by an internal integrity check for the app... (40%)" load_all_containers - # [D] Set Consul + + display_checkpoint_message "Reached the transition point... (65%)" + display_immediate_transition ${state} ${new_state} ./nginx-blue-green-activate.sh ${new_state} ${state} ${new_upstream} ${consul_key_value_store} # [E] External Integrity Check, if fails, 'emergency-nginx-down-and-up.sh' will be run. + display_checkpoint_message "Performing external integrity check. If it fails, 'emergency-nginx-down-and-up.sh' will be executed... (87%)" re=$(check_availability_out_of_container | tail -n 1); if [[ ${re} != 'true' ]]; then - echo "[WARNING] ! ${new_state}'s availability issue found. Now we are going to run 'emergency-nginx-down-and-up.sh' immediately." + display_checkpoint_message "[WARNING] ! ${new_state}'s availability issue found. Now we are going to run 'emergency-nginx-down-and-up.sh' immediately." bash emergency-nginx-down-and-up.sh re=$(check_availability_out_of_container | tail -n 1); @@ -281,6 +309,7 @@ _main() { # [F] Finalizing the process : from this point on, regarded as "success". + display_checkpoint_message "Finalizing the process. From this point, the deployment will be regarded as successful. (99%)" if [[ ${skip_building_app_image} != 'true' ]]; then backup_to_new_images fi @@ -290,6 +319,7 @@ _main() { echo "[NOTICE] For safety, finally check Consul pointing before stopping the previous container (${initially_cached_old_state})." local consul_pointing=$(docker exec ${project_name}-nginx curl ${consul_key_value_store}?raw 2>/dev/null || echo "failed") if [[ ${consul_pointing} != ${initially_cached_old_state} ]]; then + if [[ ${orchestration_type} != 'stack' ]]; then docker-compose -f docker-${orchestration_type}-${project_name}-${app_env}.yml stop ${project_name}-${initially_cached_old_state} echo "[NOTICE] The previous (${initially_cached_old_state}) container (initially_cached_old_state) has been stopped because the deployment was successful. (If NGINX_RESTART=true or CONSUL_RESTART=true, existing containers have already been terminated in the load_all_containers function.)" @@ -297,14 +327,17 @@ _main() { docker stack rm ${project_name}-${initially_cached_old_state} echo "[NOTICE] The previous (${initially_cached_old_state}) service (initially_cached_old_state) has been stopped because the deployment was successful. (If NGINX_RESTART=true or CONSUL_RESTART=true, existing containers have already been terminated in the load_all_containers function.)" fi + + display_checkpoint_message "CURRENT APP_URL: ${app_url}. Run 'bash check-current-states.sh' whenever you want to check the deployment status and Git SHA." + print_git_sha_and_message "${project_name}-${new_state}" "$docker_build_sha_insert_git_root" + + echo "[NOTICE] Delete : images." + docker rmi $(docker images -f "dangling=true" -q) || echo "[NOTICE] Any images in use will not be deleted." + else echo "[NOTICE] The previous (${initially_cached_old_state}) container (initially_cached_old_state) has NOT been stopped because the current Consul Pointing is ${consul_pointing}." fi - echo "[NOTICE] Delete : images." - docker rmi $(docker images -f "dangling=true" -q) || echo "[NOTICE] Any images in use will not be deleted." - - echo "[NOTICE] APP_URL : ${app_url}" } _main diff --git a/set-safe-permissions.sh b/set-safe-permissions.sh deleted file mode 100644 index 7ef4920..0000000 --- a/set-safe-permissions.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash -set -eu - -source use-common.sh -check_gnu_sed_installed - -sudo sed -i -e "s/\r$//g" $(basename $0) || sed -i -e "s/\r$//g" $(basename $0) -sudo chmod 770 * -sudo chmod 770 .env.* -sudo chmod 770 tests/* -sudo chmod -R 770 .docker/nginx -# This is temporary. You should set your SSLs to be such as 644 or 640 on your SSL folder. -sudo chmod -R 770 .docker/ssl diff --git a/stop-all-containers.sh b/stop-all-containers.sh index 914b4d5..9575b15 100644 --- a/stop-all-containers.sh +++ b/stop-all-containers.sh @@ -1,8 +1,10 @@ #!/bin/bash source use-common.sh + check_bash_version check_gnu_grep_installed check_gnu_sed_installed +check_yq_installed check_git_docker_compose_commands_exist sudo sed -i -e "s/\r$//g" $(basename $0) diff --git a/use-app.sh b/use-app.sh index 5a2510e..0cca2ec 100644 --- a/use-app.sh +++ b/use-app.sh @@ -35,14 +35,14 @@ apply_env_service_name_onto_app_yaml(){ check_yq_installed if [[ ${orchestration_type} == 'stack' ]]; then - yq -i "with(.services; with_entries(select(.key ==\"*-${new_state}\") | .key |= \"${project_name}-${new_state}\"))" docker-${orchestration_type}-${project_name}-${app_env}.yml || (echo "[ERROR] Failed to apply the green service name in the app YAML as ${project_name}." && exit 1) - # yq eval '(.services.[] | select(.image == "${PROJECT_NAME}:blue")).image |= \"${project_name}-blue\"' -i docker-${orchestration_type}-${project_name}-blue.yml || (echo "[ERROR] Failed to apply image : ${project_name}-blue in the app YAML." && exit 1) - yq -i "(.services.\"${project_name}-${new_state}\").image = \"${project_name}:${new_state}\"" -i docker-${orchestration_type}-${project_name}-${app_env}.yml || (echo "[ERROR] Failed to apply image : ${project_name}-${new_state} in the app YAML." && exit 1) + bin/yq -i "with(.services; with_entries(select(.key ==\"*-${new_state}\") | .key |= \"${project_name}-${new_state}\"))" docker-${orchestration_type}-${project_name}-${app_env}.yml || (echo "[ERROR] Failed to apply the green service name in the app YAML as ${project_name}." && exit 1) + # bin/yq eval '(.services.[] | select(.image == "${PROJECT_NAME}:blue")).image |= \"${project_name}-blue\"' -i docker-${orchestration_type}-${project_name}-blue.yml || (echo "[ERROR] Failed to apply image : ${project_name}-blue in the app YAML." && exit 1) + bin/yq -i "(.services.\"${project_name}-${new_state}\").image = \"${project_name}:${new_state}\"" -i docker-${orchestration_type}-${project_name}-${app_env}.yml || (echo "[ERROR] Failed to apply image : ${project_name}-${new_state} in the app YAML." && exit 1) else echo "[NOTICE] PROJECT_NAME on .env is now being applied to docker-${orchestration_type}-${project_name}-${app_env}.yml." - yq -i "with(.services; with_entries(select(.key ==\"*-blue\") | .key |= \"${project_name}-blue\"))" docker-${orchestration_type}-${project_name}-${app_env}.yml || (echo "[ERROR] Failed to apply the blue service name in the app YAML as ${project_name}." && exit 1) + bin/yq -i "with(.services; with_entries(select(.key ==\"*-blue\") | .key |= \"${project_name}-blue\"))" docker-${orchestration_type}-${project_name}-${app_env}.yml || (echo "[ERROR] Failed to apply the blue service name in the app YAML as ${project_name}." && exit 1) sleep 2 - yq -i "with(.services; with_entries(select(.key ==\"*-green\") | .key |= \"${project_name}-green\"))" docker-${orchestration_type}-${project_name}-${app_env}.yml || (echo "[ERROR] Failed to apply the green service name in the app YAML as ${project_name}." && exit 1) + bin/yq -i "with(.services; with_entries(select(.key ==\"*-green\") | .key |= \"${project_name}-green\"))" docker-${orchestration_type}-${project_name}-${app_env}.yml || (echo "[ERROR] Failed to apply the green service name in the app YAML as ${project_name}." && exit 1) fi } @@ -61,21 +61,42 @@ apply_docker_compose_environment_onto_app_yaml(){ for state in "${states[@]}" do - yq -i '.services.'${project_name}'-'${state}'.environment = []' docker-${orchestration_type}-${project_name}-${app_env}.yml - yq -i '.services.'${project_name}'-'${state}'.environment += "SERVICE_NAME='${state}'"' docker-${orchestration_type}-${project_name}-${app_env}.yml + bin/yq -i '.services.'${project_name}'-'${state}'.environment = []' docker-${orchestration_type}-${project_name}-${app_env}.yml + bin/yq -i '.services.'${project_name}'-'${state}'.environment += "SERVICE_NAME='${state}'"' docker-${orchestration_type}-${project_name}-${app_env}.yml - for ((i=1; i<=$(echo ${docker_compose_environment} | yq eval 'length'); i++)) + for ((i=1; i<=$(echo ${docker_compose_environment} | bin/yq eval 'length'); i++)) do - yq -i '.services.'${project_name}'-'${state}'.environment += "'$(echo ${docker_compose_environment} | yq -r 'to_entries | .['$((i-1))'].key')'='$(echo ${docker_compose_environment} | yq -r 'to_entries | .['$((i-1))'].value')'"' docker-${orchestration_type}-${project_name}-${app_env}.yml + bin/yq -i '.services.'${project_name}'-'${state}'.environment += "'$(echo ${docker_compose_environment} | bin/yq -r 'to_entries | .['$((i-1))'].key')'='$(echo ${docker_compose_environment} | bin/yq -r 'to_entries | .['$((i-1))'].value')'"' docker-${orchestration_type}-${project_name}-${app_env}.yml done done } +check_docker_compose_real_host_volumes_directories() { + + local volumes=$(echo "${docker_compose_real_selective_volumes[@]}" | tr -d '[]"') + + for volume in ${volumes} + do + # Extract the local directory path before the colon (:) + local_dir="${volume%%:*}" + + # Check if the directory or file exists + if [[ ! -f "$local_dir" && ! -d "$local_dir" ]]; then + echo "[ERROR] The local path '$local_dir' specified in DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES does not exist. Exiting..." + exit 1 + fi + done +} + apply_docker_compose_volumes_onto_app_real_yaml(){ check_yq_installed + if [[ ${docker_compose_host_volume_check} == 'true' ]]; then + check_docker_compose_real_host_volumes_directories + fi + echo "[NOTICE] DOCKER_COMPOSE_REAL_SELECTIVE_VOLUMES on .env is now being applied to docker-${orchestration_type}-${project_name}-real.yml." if [[ ${orchestration_type} == 'stack' ]]; then @@ -86,11 +107,11 @@ apply_docker_compose_volumes_onto_app_real_yaml(){ for state in "${states[@]}" do - #yq -i '.services.'${project_name}'-'${state}'.volumes = []' ./docker-${orchestration_type}-${project_name}-real.yml + #bin/yq -i '.services.'${project_name}'-'${state}'.volumes = []' ./docker-${orchestration_type}-${project_name}-real.yml for volume in "${docker_compose_real_selective_volumes[@]}" do - yq -i '.services.'${project_name}'-'${state}'.volumes += '${volume}'' ./docker-${orchestration_type}-${project_name}-real.yml + bin/yq -i '.services.'${project_name}'-'${state}'.volumes += '${volume}'' ./docker-${orchestration_type}-${project_name}-real.yml done done @@ -105,9 +126,9 @@ make_docker_build_arg_strings(){ local re="" - for ((i=1; i<=$(echo ${docker_build_args} | yq eval 'length'); i++)) + for ((i=1; i<=$(echo ${docker_build_args} | bin/yq eval 'length'); i++)) do - re="${re} --build-arg $(echo ${docker_build_args} | yq -r 'to_entries | .['$((i-1))'].key')=$(echo ${docker_build_args} | yq -r 'to_entries | .['$((i-1))'].value')" + re="${re} --build-arg $(echo ${docker_build_args} | bin/yq -r 'to_entries | .['$((i-1))'].key')=$(echo ${docker_build_args} | bin/yq -r 'to_entries | .['$((i-1))'].value')" done echo ${re} @@ -132,14 +153,36 @@ load_app_docker_image() { echo "[NOTICE] Build the image with ${docker_file_location}/${docker_file_name} (using cache)" local env_build_args=$(make_docker_build_arg_strings) echo "[NOTICE] DOCKER_BUILD_ARGS on the .env : ${env_build_args}" + echo "[NOTICE] DOCKER_BUILD_LABELS on the .env : ${docker_build_labels}" + + # Convert DOCKER_BUILD_LABELS to Docker --label arguments + local label_args="" + IFS=',' read -r -a labels <<< "$docker_build_labels" + + for label in "${labels[@]}"; do + # Ensure labels are in correct format (e.g., key=value) + formatted_label=$(echo "$label" | sed -e 's/^\[//' -e 's/\]$//' -e 's/^"//' -e 's/"$//' | xargs) + + # Only add to label_args if formatted_label is not empty + if [[ -n "$formatted_label" ]]; then + label_args+="--label $formatted_label " + fi + done + + # Trim whitespace and check if label_args is just "--label" + if [[ $(echo "$label_args" | xargs) == "--label" ]]; then + label_args="" + fi + + echo "[NOTICE] Final DOCKER_BUILD_LABELS on the .env : ${label_args} " if [[ ${docker_layer_corruption_recovery} == true ]]; then echo "[NOTICE] Docker Build Command : docker build --no-cache --tag ${project_name}:latest --build-arg server="${app_env}" ${env_build_args} -f ${docker_file_name} -m ${docker_build_memory_usage} ." - cd ${docker_file_location} && docker build --no-cache --tag ${project_name}:latest --build-arg server="${app_env}" ${env_build_args} -f ${docker_file_name} -m ${docker_build_memory_usage} . || exit 1 + cd ${docker_file_location} && docker build --no-cache --tag ${project_name}:latest ${label_args} --build-arg server="${app_env}" ${env_build_args} -f ${docker_file_name} -m ${docker_build_memory_usage} . || exit 1 cd - else echo "[NOTICE] Docker Build Command : docker build --build-arg DISABLE_CACHE=${CUR_TIME} --tag ${project_name}:latest --build-arg server="${app_env}" --build-arg HOST_IP="${HOST_IP}" ${env_build_args} -f ${docker_file_name} -m ${docker_build_memory_usage} ." - cd ${docker_file_location} && docker build --build-arg DISABLE_CACHE=${CUR_TIME} --tag ${project_name}:latest --build-arg server="${app_env}" --build-arg HOST_IP="${HOST_IP}" ${env_build_args} -f ${docker_file_name} -m ${docker_build_memory_usage} . || exit 1 + cd ${docker_file_location} && docker build --build-arg DISABLE_CACHE=${CUR_TIME} --tag ${project_name}:latest ${label_args} --build-arg server="${app_env}" --build-arg HOST_IP="${HOST_IP}" ${env_build_args} -f ${docker_file_name} -m ${docker_build_memory_usage} . || exit 1 cd - fi @@ -252,6 +295,7 @@ check_availability_inside_container(){ echo "[NOTICE] [Internal Integrity Check : will deploy ${check_state}] In the ${container_name} Container, conduct the Connection Check (localhost:${app_port} --timeout=${2}). (If this is delayed, run ' docker logs -f ${container_name} (compose), docker service ps ${project_name}-${check_state}_${project_name}-${check_state} (stack) ' to check the status." >&2 echo "[NOTICE] [Internal Integrity Check : will deploy ${check_state}] Current status (inside Container) : \n $(docker logs ${container_name})" >&2 + echo "[NOTICE] wait_for_it ( https://github.com/vishnubob/wait-for-it ) will do the Check" >&2 local wait_for_it_re=$(docker exec -w ${project_location} ${container_name} ./wait-for-it.sh localhost:${app_port} --timeout=${2}) || (echo "[ERROR] Failed in running (CONTAINER : ${project_location}/wait-for-it.sh)" >&2 && echo "false" && return) if [[ $? != 0 ]]; then echo "[ERROR] Failed in getting the correct return from wait-for-it.sh. (${wait_for_it_re})" >&2 @@ -272,7 +316,7 @@ check_availability_inside_container(){ for (( retry_count = 1; retry_count <= ${total_cnt}; retry_count++ )) do - echo "[NOTICE] ${retry_count} round health check (curl -s -k ${app_https_protocol}://$(concat_safe_port localhost)/${app_health_check_path})... (timeout : ${3} sec)" >&2 + echo -e "\033[1;35m[NOTICE] ${retry_count} round health check (curl -s -k ${app_https_protocol}://$(concat_safe_port localhost)/${app_health_check_path})... (timeout: ${3} sec)\033[0m" >&2 response=$(docker exec ${container_name} sh -c "curl -s -k ${app_https_protocol}://$(concat_safe_port localhost)/${app_health_check_path} --connect-timeout ${3}") down_count=$(echo ${response} | grep -Ei ${bad_app_health_check_pattern} | wc -l) @@ -281,7 +325,14 @@ check_availability_inside_container(){ if [[ ${down_count} -ge 1 || ${up_count} -lt 1 ]] then - echo "[WARNING] Unable to determine the response of the health check or the status is not UP, or Check the REDIRECT_HTTPS_TO_HTTP param in .env (*Response : ${response}), (${container_name}, *Log (print max 25 lines) : $(docker logs --tail 25 ${container_name})" >&2 + echo -e "\033[1;35m[WARNING] Unable to determine the health check response, or the status is not UP. Please check the following:\033[0m + \033[1;35m1) Verify the REDIRECT_HTTPS_TO_HTTP parameter in .env\033[0m + \033[1;35m2) Confirm your app's database connection\033[0m + \033[1;35m3) Review your health check settings in .env.\033[0m + \033[1;35m*Response:\033[0m ${response} + \033[1;35m*Container:\033[0m ${container_name} + \033[1;35m*Logs (last 25 lines):\033[0m $(docker logs --tail 25 ${container_name})" >&2 + else echo "[NOTICE] Internal health check of the application succeeded. (*Response: ${response})" >&2 diff --git a/use-common.sh b/use-common.sh index 02fbcfd..7fac559 100644 --- a/use-common.sh +++ b/use-common.sh @@ -4,10 +4,116 @@ set -eu source ./validator.sh source ./use-states.sh +display_emphasized_message() { + local message=$1 + printf "\033[1;34m%s\033[0m\n" "$message" # Display message in bold blue +} + +display_checkpoint_message() { + local message=$1 + printf "\033[1;34m[CHECKPOINT] %s\033[0m\n" "$message" # Display message in bold blue +} + +# Function to display a transition message between states in a Blue-Green deployment +display_planned_transition() { + local current_state=$1 + local target_state=$2 + + # Clear the screen and set text to bold blue + echo -e "\033[1;34m" # Bold blue text + + echo "─────────────────────────────" + echo " Current State (${current_state})" + echo "─────────────────────────────" + echo " |" + echo " >> Transition planned <<" + echo " v" + echo "─────────────────────────────" + echo " Target State (${target_state})" + echo "─────────────────────────────" + echo -e "\033[0m" # Reset text style +} + +display_immediate_transition() { + local current_state=$1 + local target_state=$2 + + # Display the state transition diagram with a bold blue message + echo -e "\033[1;34m" # Bold blue text + + echo "─────────────────────────────" + echo " Current State (${current_state})" + echo "─────────────────────────────" + echo " |" + echo " >> Immediate Transition <<" + echo " v" + echo "─────────────────────────────" + echo " Target State (${target_state})" + echo "─────────────────────────────" + echo -e "\033[0m" # Reset text style + echo -e "\033[1;32m" # Bold green text for emphasis + echo ">>> Transition to ${target_state} is now being executed <<<" + echo -e "\033[0m" # Reset text style +} + + to_lower() { echo "$1" | tr '[:upper:]' '[:lower:]' } +get_git_sha_or_none() { + local target_dir=$1 # Accepts the directory as the first argument + local sha="" + + # Check if the directory exists and is a Git repository + if [ -d "$target_dir" ] && [ -d "$target_dir/.git" ]; then + # Navigate to the target directory + pushd "$target_dir" > /dev/null + # Retrieve the SHA value and store it in the variable + sha=$(git rev-parse HEAD) + # Return to the original directory + popd > /dev/null + else + echo "NONE" + fi + + # Output the SHA value + echo "$sha" +} + +# Function to retrieve the SHA label from the container +get_container_git_sha() { + local container_name=$1 + docker inspect -f '{{ index .Config.Labels "project.git.sha" }}' "$container_name" 2>/dev/null || echo "" +} + +# Function to retrieve the commit message for a given SHA +get_commit_message() { + local sha=$1 + local git_location=$2 + git -C "$git_location" log -1 --pretty=format:"%s" "$sha" 2>/dev/null || echo "Commit message NOT found (docker_build_sha_insert_git_root : $git_location)" +} + +# Function to print SHA and commit message +print_git_sha_and_message() { + local container_name=$1 + local git_location=$2 + + # Retrieve SHA + local sha=$(get_container_git_sha "$container_name") + + # Check if SHA was found + if [[ -n "$sha" ]]; then + # Retrieve commit message + local commit_message=$(get_commit_message "$sha" "$git_location") + + echo "[NOTICE] Git SHA for the Current Container: $sha" + echo "[NOTICE] Git Commit Message for the Current SHA: $commit_message" + else + echo "[NOTICE] The 'project.git.sha' label is missing for the container '${container_name}', so the container's Git information cannot be retrieved. It appears the container was created with 'DOCKER_BUILD_SHA_INSERT_GIT_ROOT' left empty in the .env file." + fi +} + set_expose_and_app_port(){ if [[ -z ${1} ]] @@ -16,8 +122,8 @@ set_expose_and_app_port(){ fi if echo "${1}" | grep -Eq '^\[[0-9]+,[0-9]+\]$'; then - expose_port=$(echo "$project_port" | yq e '.[0]' -) - app_port=$(echo "$project_port" | yq e '.[1]' -) + expose_port=$(echo "$project_port" | bin/yq e '.[0]' -) + app_port=$(echo "$project_port" | bin/yq e '.[1]' -) else expose_port="$project_port" app_port="$project_port" @@ -57,6 +163,19 @@ cache_non_dependent_global_vars() { docker_compose_environment=$(get_value_from_env "DOCKER_COMPOSE_ENVIRONMENT") docker_build_args=$(get_value_from_env "DOCKER_BUILD_ARGS") + # Get DOCKER_BUILD_LABELS and Git SHA + docker_build_labels=$(get_value_from_env "DOCKER_BUILD_LABELS") + + # and Git SHA + docker_build_sha_insert_git_root=$(get_value_from_env "DOCKER_BUILD_SHA_INSERT_GIT_ROOT") + + + project_git_sha="" + if [[ -n "${docker_build_sha_insert_git_root}" ]]; then + project_git_sha=$(get_git_sha_or_none "${docker_build_sha_insert_git_root}") + docker_build_labels="${docker_build_labels},project.git.sha=${project_git_sha}" + fi + consul_key_value_store=$(get_value_from_env "CONSUL_KEY_VALUE_STORE") consul_key=$(echo ${consul_key_value_store} | cut -d "/" -f6)\\/$(echo ${consul_key_value_store} | cut -d "/" -f7) @@ -86,6 +205,7 @@ cache_non_dependent_global_vars() { fi docker_compose_nginx_selective_volumes=$(get_value_from_env "DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES") + docker_compose_host_volume_check=$(get_value_from_env "DOCKER_COMPOSE_HOST_VOLUME_CHECK") docker_layer_corruption_recovery=$(get_value_from_env "DOCKER_LAYER_CORRUPTION_RECOVERY") @@ -205,25 +325,67 @@ cache_global_vars() { check_yq_installed(){ required_version="4.35.1" + yq_path="bin/yq" + + # Function to download yq + download_yq() { + echo "[NOTICE] Downloading bin/yq version $required_version..." >&2 + + # Detect OS and architecture + ARCH=$(uname -m) + OS=$(uname | tr '[:upper:]' '[:lower:]') + + # Determine the correct bin/yq binary based on architecture + case "$OS-$ARCH" in + linux-x86_64) + YQ_BINARY="yq_linux_amd64" + ;; + linux-aarch64) + YQ_BINARY="yq_linux_arm64" + ;; + linux-armv7l | linux-armhf) + YQ_BINARY="yq_linux_arm" + ;; + linux-i386 | linux-i686) + YQ_BINARY="yq_linux_386" + ;; + darwin-x86_64) + YQ_BINARY="yq_darwin_amd64" + ;; + darwin-arm64) + YQ_BINARY="yq_darwin_arm64" + ;; + *) + echo >&2 "[ERROR] Unsupported OS or architecture: $OS-$ARCH" + exit 1 + ;; + esac + + DOWNLOAD_URL="https://github.com/mikefarah/yq/releases/download/v$required_version/$YQ_BINARY" + + # Download yq + curl -L "$DOWNLOAD_URL" -o "$yq_path" + chmod +x "$yq_path" + echo "[NOTICE] bin/yq version $required_version from $DOWNLOAD_URL has been downloaded to $yq_path." >&2 + } - # Check if yq is installed - if ! command -v yq >/dev/null 2>&1; then - echo >&2 "[ERROR] yq is NOT installed. Please install yq version $required_version manually." - echo >&2 "You can download it from the following URL:" - echo >&2 "https://github.com/mikefarah/yq/releases/download/v$required_version/yq_linux_amd64" - exit 1 + # Check if bin/yq is installed in the bin directory + if [ ! -f "$yq_path" ]; then + echo "[WARNING] bin/yq is not found in $yq_path. Downloading..." >&2 + download_yq else - # Check if installed version is not 4.35.1 - installed_version=$(yq --version | grep -oP 'version v\K[0-9.]+') + # Check if installed bin/yq version is the required version + installed_version=$("$yq_path" --version | grep -oP 'version v\K[0-9.]+') if [ "$installed_version" != "$required_version" ]; then - echo >&2 "[ERROR] yq version is $installed_version. Please install yq version $required_version manually." - echo >&2 "You can download it from the following URL:" - echo >&2 "https://github.com/mikefarah/yq/releases/download/v$required_version/yq_linux_amd64" - exit 1 + echo "[WARNING] bin/yq version is $installed_version, which is not the required version $required_version." >&2 + download_yq + else + echo "[NOTICE] bin/yq version $required_version is already installed in $yq_path." >&2 fi fi } + check_gnu_grep_installed() { # Check if grep is installed if ! command -v grep >/dev/null 2>&1; then @@ -347,7 +509,7 @@ get_value_from_env(){ value=$(echo $value | sed -e 's/\r//g') if [[ -z ${value} ]]; then - echo "[WARNING] ${value} for the key ${1} is empty .env." >&2 + echo "[WARNING] The value for the key ${1} is empty (value : ${value}) .env." >&2 fi echo ${value} # return. @@ -391,7 +553,7 @@ check_empty_env_values(){ value="$(echo -e "${value}" | sed -e 's/^[[:space:]]*|[[:space:]]*$//')" - if [[ ${value} == '' && ${key} != "CONTAINER_SSL_VOLUME_PATH" && ${key} != "ADDITIONAL_PORTS" && ${key} != "UIDS_BELONGING_TO_SHARED_VOLUME_GROUP_ID" ]]; then + if [[ ${value} == '' && ${key} != "CONTAINER_SSL_VOLUME_PATH" && ${key} != "ADDITIONAL_PORTS" && ${key} != "UIDS_BELONGING_TO_SHARED_VOLUME_GROUP_ID" && ${key} != "DOCKER_BUILD_LABELS" && ${key} != "DOCKER_BUILD_SHA_INSERT_GIT_ROOT" ]]; then empty_keys+=(${key}) fi @@ -619,4 +781,4 @@ stop_and_remove_container() { else echo "[NOTICE] Container ${container_name} does not exist." fi -} \ No newline at end of file +} diff --git a/use-nginx.sh b/use-nginx.sh index c4b44c3..e2830a0 100644 --- a/use-nginx.sh +++ b/use-nginx.sh @@ -9,32 +9,55 @@ initiate_nginx_docker_compose_file(){ echo "[DEBUG] successfully copied docker-compose-app-nginx-original.yml" } apply_env_service_name_onto_nginx_yaml(){ - yq -i "with(.services; with_entries(select(.key ==\"*-nginx\") | .key |= \"${project_name}-nginx\"))" docker-compose-${project_name}-nginx.yml || (echo "[ERROR] Failed to apply the service name in the Nginx YML as ${project_name}." && exit 1) + bin/yq -i "with(.services; with_entries(select(.key ==\"*-nginx\") | .key |= \"${project_name}-nginx\"))" docker-compose-${project_name}-nginx.yml || (echo "[ERROR] Failed to apply the service name in the Nginx YML as ${project_name}." && exit 1) } apply_ports_onto_nginx_yaml(){ check_yq_installed echo "[NOTICE] PORTS on .env is now being applied to docker-compose-${project_name}-nginx.yml." - yq -i '.services.'${project_name}'-nginx.ports = []' docker-compose-${project_name}-nginx.yml - yq -i '.services.'${project_name}'-nginx.ports += "'${expose_port}':'${expose_port}'"' docker-compose-${project_name}-nginx.yml + bin/yq -i '.services.'${project_name}'-nginx.ports = []' docker-compose-${project_name}-nginx.yml + bin/yq -i '.services.'${project_name}'-nginx.ports += "'${expose_port}':'${expose_port}'"' docker-compose-${project_name}-nginx.yml for i in "${additional_ports[@]}" do [ -z "${i##*[!0-9]*}" ] && (echo "[ERROR] Wrong port number on .env : ${i}" && exit 1); - yq -i '.services.'${project_name}'-nginx.ports += "'$i:$i'"' docker-compose-${project_name}-nginx.yml + bin/yq -i '.services.'${project_name}'-nginx.ports += "'$i:$i'"' docker-compose-${project_name}-nginx.yml done } + +check_docker_compose_nginx_host_volumes_directories() { + + local volumes=$(echo "${docker_compose_nginx_selective_volumes[@]}" | tr -d '[]"') + + for volume in ${volumes} + do + # Extract the local directory path before the colon (:) + local_dir="${volume%%:*}" + + # Check if the directory or file exists + if [[ ! -f "$local_dir" && ! -d "$local_dir" ]]; then + echo "[ERROR] The local path '$local_dir' specified in DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES does not exist. Exiting..." + exit 1 + fi + done +} + + apply_docker_compose_volumes_onto_app_nginx_yaml(){ check_yq_installed + if [[ ${docker_compose_host_volume_check} == 'true' ]]; then + check_docker_compose_nginx_host_volumes_directories + fi + echo "[NOTICE] DOCKER_COMPOSE_NGINX_SELECTIVE_VOLUMES on .env is now being applied to docker-compose-${project_name}-nginx.yml." for volume in "${docker_compose_nginx_selective_volumes[@]}" do - yq -i '.services.'${project_name}'-'nginx'.volumes += '${volume}'' ./docker-compose-${project_name}-nginx.yml + bin/yq -i '.services.'${project_name}'-'nginx'.volumes += '${volume}'' ./docker-compose-${project_name}-nginx.yml done } @@ -293,7 +316,7 @@ nginx_down(){ nginx_up(){ echo "[NOTICE] Up NGINX Container." - PROJECT_NAME=${project_name} docker-compose -f docker-compose-${project_name}-nginx.yml up -d || echo "[ERROR] Critical - ${project_name}-nginx UP failure" + PROJECT_NAME=${project_name} docker-compose -f docker-compose-${project_name}-nginx.yml up -d || echo "[ERROR] Critical - ${project_name}-nginx UP failure." }