diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000000..1d98e4f5b6 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,26 @@ +name: Docker Image CI +on: + push: + tags: + - 'v*' + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Docker Setup Buildx + uses: docker/setup-buildx-action@v2 + - name: Login to Azure Container Registry + uses: Azure/docker-login@v1 + with: + username: ${{ secrets.ACR_USERNAME }} + password: ${{ secrets.ACR_PASSWORD }} + login-server: ${{ secrets.ACR_SERVER }} + - name: Build and push Docker image + run: | + docker buildx create --name mybuilder --driver docker-container --platform linux/amd64,linux/arm64 --bootstrap --use + docker buildx build --platform linux/amd64,linux/arm64 --tag ${{ secrets.ACR_SERVER }}/azure-vote-front:$(echo "${{ github.ref }}" | grep -oP 'refs/tags/\K(.+)') --push ./azure-vote \ No newline at end of file diff --git a/azure-vote-all-in-one-redis.yaml b/azure-vote-all-in-one-redis.yaml index e971a76092..0c64b7dbc2 100644 --- a/azure-vote-all-in-one-redis.yaml +++ b/azure-vote-all-in-one-redis.yaml @@ -13,10 +13,10 @@ spec: app: azure-vote-back spec: nodeSelector: - "beta.kubernetes.io/os": linux + "kubernetes.io/os": linux containers: - name: azure-vote-back - image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + image: redis:latest env: - name: ALLOW_EMPTY_PASSWORD value: "yes" @@ -54,10 +54,10 @@ spec: app: azure-vote-front spec: nodeSelector: - "beta.kubernetes.io/os": linux + "kubernetes.io/os": linux containers: - name: azure-vote-front - image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 + image: cloudnativeadvocates.azurecr.io/azure-vote-front:v1.0.0 ports: - containerPort: 80 resources: diff --git a/azure-vote/Dockerfile b/azure-vote/Dockerfile index ac103827a0..f0a2f92af2 100644 --- a/azure-vote/Dockerfile +++ b/azure-vote/Dockerfile @@ -1,3 +1,99 @@ -FROM tiangolo/uwsgi-nginx-flask:python3.6 -RUN pip install redis -ADD /azure-vote /app +# sources: +# https://github.com/tiangolo/uwsgi-nginx-docker/blob/master/docker-images/python3.6.dockerfile +# https://github.com/tiangolo/uwsgi-nginx-flask-docker/blob/master/docker-images/python3.6.dockerfile + +FROM python:3.9-buster + +#LABEL maintainer="Sebastian Ramirez " + +COPY install-nginx-debian.sh / + +RUN bash /install-nginx-debian.sh + +# Install requirements +COPY requirements.txt /tmp/requirements.txt +RUN pip install --no-cache-dir -r /tmp/requirements.txt + +EXPOSE 80 + +# Expose 443, in case of LTS / HTTPS +EXPOSE 443 + +# Remove default configuration from Nginx +RUN rm /etc/nginx/conf.d/default.conf +# Copy the base uWSGI ini file to enable default dynamic uwsgi process number +COPY uwsgi.ini /etc/uwsgi/ + +# Install Supervisord +RUN apt-get update && apt-get install -y supervisor \ + && rm -rf /var/lib/apt/lists/* +# Custom Supervisord config +COPY supervisord-debian.conf /etc/supervisor/conf.d/supervisord.conf + +# Copy stop-supervisor.sh to kill the supervisor and substasks on app failure +COPY stop-supervisor.sh /etc/supervisor/stop-supervisor.sh +RUN chmod +x /etc/supervisor/stop-supervisor.sh + +# Which uWSGI .ini file should be used, to make it customizable +ENV UWSGI_INI /app/uwsgi.ini + +# By default, run 2 processes +ENV UWSGI_CHEAPER 2 + +# By default, when on demand, run up to 16 processes +ENV UWSGI_PROCESSES 16 + +# By default, allow unlimited file sizes, modify it to limit the file sizes +# To have a maximum of 1 MB (Nginx's default) change the line to: +# ENV NGINX_MAX_UPLOAD 1m +ENV NGINX_MAX_UPLOAD 0 + +# By default, Nginx will run a single worker process, setting it to auto +# will create a worker for each CPU core +ENV NGINX_WORKER_PROCESSES 1 + +# By default, Nginx listens on port 80. +# To modify this, change LISTEN_PORT environment variable. +# (in a Dockerfile or with an option for `docker run`) +ENV LISTEN_PORT 80 + +# URL under which static (not modified by Python) files will be requested +# They will be served by Nginx directly, without being handled by uWSGI +ENV STATIC_URL /static + +# Absolute path in where the static files wil be +ENV STATIC_PATH /app/static + +# If STATIC_INDEX is 1, serve / with /static/index.html directly (or the static URL configured) +# ENV STATIC_INDEX 1 +ENV STATIC_INDEX 0 + +# Make /app/* available to be imported by Python globally to better support several use cases like Alembic migrations. +ENV PYTHONPATH=/app + +# Copy start.sh script that will check for a /app/prestart.sh script and run it before starting the app +COPY start.sh /start.sh +RUN chmod +x /start.sh + +# Copy the entrypoint that will generate Nginx additional configs +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# Add demo app +COPY ./azure-vote /app +WORKDIR /app +RUN ls + +# Copy the base uwsgi-nginx-entrypoint to reuse it +COPY uwsgi-nginx-entrypoint.sh /uwsgi-nginx-entrypoint.sh +RUN chmod +x /uwsgi-nginx-entrypoint.sh + +# Copy the entrypoint that will generate Nginx additional configs +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] + +# Run the start script, it will check for an /app/prestart.sh script (e.g. for migrations) +# And then will start Supervisor, which in turn will start Nginx and uWSGI +CMD ["/start.sh"] diff --git a/azure-vote/azure-vote/prestart.sh b/azure-vote/azure-vote/prestart.sh new file mode 100644 index 0000000000..70764ba84b --- /dev/null +++ b/azure-vote/azure-vote/prestart.sh @@ -0,0 +1,11 @@ +#! /usr/bin/env sh + +echo "Running inside /app/prestart.sh, you could add migrations to this file, e.g.:" + +echo " +#! /usr/bin/env sh +# Let the DB start +sleep 10; +# Run migrations +alembic upgrade head +" \ No newline at end of file diff --git a/azure-vote/azure-vote/uwsgi.ini b/azure-vote/azure-vote/uwsgi.ini new file mode 100644 index 0000000000..4e79fbdf4a --- /dev/null +++ b/azure-vote/azure-vote/uwsgi.ini @@ -0,0 +1,3 @@ +[uwsgi] +module = main +callable = app \ No newline at end of file diff --git a/azure-vote/entrypoint.sh b/azure-vote/entrypoint.sh new file mode 100644 index 0000000000..3dddb4b131 --- /dev/null +++ b/azure-vote/entrypoint.sh @@ -0,0 +1,46 @@ +#! /usr/bin/env sh +set -e + +/uwsgi-nginx-entrypoint.sh + +# Get the URL for static files from the environment variable +USE_STATIC_URL=${STATIC_URL:-'/static'} +# Get the absolute path of the static files from the environment variable +USE_STATIC_PATH=${STATIC_PATH:-'/app/static'} +# Get the listen port for Nginx, default to 80 +USE_LISTEN_PORT=${LISTEN_PORT:-80} + +if [ -f /app/nginx.conf ]; then + cp /app/nginx.conf /etc/nginx/nginx.conf +else + content_server='server {\n' + content_server=$content_server" listen ${USE_LISTEN_PORT};\n" + content_server=$content_server' location / {\n' + content_server=$content_server' try_files $uri @app;\n' + content_server=$content_server' }\n' + content_server=$content_server' location @app {\n' + content_server=$content_server' include uwsgi_params;\n' + content_server=$content_server' uwsgi_pass unix:///tmp/uwsgi.sock;\n' + content_server=$content_server' }\n' + content_server=$content_server" location $USE_STATIC_URL {\n" + content_server=$content_server" alias $USE_STATIC_PATH;\n" + content_server=$content_server' }\n' + # If STATIC_INDEX is 1, serve / with /static/index.html directly (or the static URL configured) + if [ "$STATIC_INDEX" = 1 ] ; then + content_server=$content_server' location = / {\n' + content_server=$content_server" index $USE_STATIC_URL/index.html;\n" + content_server=$content_server' }\n' + fi + content_server=$content_server'}\n' + # Save generated server /etc/nginx/conf.d/nginx.conf + printf "$content_server" > /etc/nginx/conf.d/nginx.conf +fi + +# For Alpine: +# Explicitly add installed Python packages and uWSGI Python packages to PYTHONPATH +# Otherwise uWSGI can't import Flask +if [ -n "$ALPINEPYTHON" ] ; then + export PYTHONPATH=$PYTHONPATH:/usr/local/lib/$ALPINEPYTHON/site-packages:/usr/lib/$ALPINEPYTHON/site-packages +fi + +exec "$@" diff --git a/azure-vote/install-nginx-debian.sh b/azure-vote/install-nginx-debian.sh new file mode 100644 index 0000000000..f546b3ffec --- /dev/null +++ b/azure-vote/install-nginx-debian.sh @@ -0,0 +1,80 @@ +#! /usr/bin/env bash + +# From official Nginx Docker image, as a script to re-use it, removing internal comments +# Ref: https://github.com/nginxinc/docker-nginx/blob/f958fbacada447737319e979db45a1da49123142/mainline/debian/Dockerfile + +# Standard set up Nginx +export NGINX_VERSION=1.21.6 +export NJS_VERSION=0.7.3 +export PKG_RELEASE=1~buster + +set -x \ + && apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y gnupg1 ca-certificates \ + && \ + NGINX_GPGKEY=573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62; \ + found=''; \ + for server in \ + ha.pool.sks-keyservers.net \ + hkp://keyserver.ubuntu.com:80 \ + hkp://p80.pool.sks-keyservers.net:80 \ + pgp.mit.edu \ + ; do \ + echo "Fetching GPG key $NGINX_GPGKEY from $server"; \ + apt-key adv --keyserver "$server" --keyserver-options timeout=10 --recv-keys "$NGINX_GPGKEY" && found=yes && break; \ + done; \ + test -z "$found" && echo >&2 "error: failed to fetch GPG key $NGINX_GPGKEY" && exit 1; \ + apt-get remove --purge --auto-remove -y gnupg1 && rm -rf /var/lib/apt/lists/* \ + && dpkgArch="$(dpkg --print-architecture)" \ + && nginxPackages=" \ + nginx=${NGINX_VERSION}-${PKG_RELEASE} \ + nginx-module-xslt=${NGINX_VERSION}-${PKG_RELEASE} \ + nginx-module-geoip=${NGINX_VERSION}-${PKG_RELEASE} \ + nginx-module-image-filter=${NGINX_VERSION}-${PKG_RELEASE} \ + nginx-module-njs=${NGINX_VERSION}+${NJS_VERSION}-${PKG_RELEASE} \ + " \ + && case "$dpkgArch" in \ + amd64|i386|arm64) \ + echo "deb https://nginx.org/packages/mainline/debian/ buster nginx" >> /etc/apt/sources.list.d/nginx.list \ + && apt-get update \ + ;; \ + *) \ + echo "deb-src https://nginx.org/packages/mainline/debian/ buster nginx" >> /etc/apt/sources.list.d/nginx.list \ + \ + && tempDir="$(mktemp -d)" \ + && chmod 777 "$tempDir" \ + \ + && savedAptMark="$(apt-mark showmanual)" \ + \ + && apt-get update \ + && apt-get build-dep -y $nginxPackages \ + && ( \ + cd "$tempDir" \ + && DEB_BUILD_OPTIONS="nocheck parallel=$(nproc)" \ + apt-get source --compile $nginxPackages \ + ) \ + \ + && apt-mark showmanual | xargs apt-mark auto > /dev/null \ + && { [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark; } \ + \ + && ls -lAFh "$tempDir" \ + && ( cd "$tempDir" && dpkg-scanpackages . > Packages ) \ + && grep '^Package: ' "$tempDir/Packages" \ + && echo "deb [ trusted=yes ] file://$tempDir ./" > /etc/apt/sources.list.d/temp.list \ + && apt-get -o Acquire::GzipIndexes=false update \ + ;; \ + esac \ + \ + && apt-get install --no-install-recommends --no-install-suggests -y \ + $nginxPackages \ + gettext-base \ + curl \ + && apt-get remove --purge --auto-remove -y && rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/nginx.list \ + \ + && if [ -n "$tempDir" ]; then \ + apt-get purge -y --auto-remove \ + && rm -rf "$tempDir" /etc/apt/sources.list.d/temp.list; \ + fi \ + && ln -sf /dev/stdout /var/log/nginx/access.log \ + && ln -sf /dev/stderr /var/log/nginx/error.log \ +# Standard set up Nginx finished diff --git a/azure-vote/requirements.txt b/azure-vote/requirements.txt new file mode 100644 index 0000000000..127cd579a1 --- /dev/null +++ b/azure-vote/requirements.txt @@ -0,0 +1,3 @@ +uwsgi==2.0.20 +flask==2.0.1 +redis==4.3.4 \ No newline at end of file diff --git a/azure-vote/start.sh b/azure-vote/start.sh new file mode 100644 index 0000000000..60718f4ca1 --- /dev/null +++ b/azure-vote/start.sh @@ -0,0 +1,15 @@ +#! /usr/bin/env sh +set -e + +# If there's a prestart.sh script in the /app directory, run it before starting +PRE_START_PATH=/app/prestart.sh +echo "Checking for script in $PRE_START_PATH" +if [ -f $PRE_START_PATH ] ; then + echo "Running script $PRE_START_PATH" + . $PRE_START_PATH +else + echo "There is no script $PRE_START_PATH" +fi + +# Start Supervisor, with Nginx and uWSGI +exec /usr/bin/supervisord diff --git a/azure-vote/stop-supervisor.sh b/azure-vote/stop-supervisor.sh new file mode 100644 index 0000000000..71f71770c6 --- /dev/null +++ b/azure-vote/stop-supervisor.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +# Ref: +# * https://github.com/tiangolo/uwsgi-nginx-docker/issues/61#issuecomment-508034634 +# * https://gist.github.com/ReallyLiri/f833510d350b242ff89b9b76fdf21ea5 +# * https://serverfault.com/a/922943 +# * https://gist.github.com/tomazzaman/63265dfab3a9a61781993212fa1057cb +# * https://gist.github.com/tomazzaman/63265dfab3a9a61781993212fa1057cb#gistcomment-2812931 +# * https://github.com/Supervisor/supervisor/issues/733 +# * +printf "READY\n"; + +while read line; do + echo "Processing Event: $line" >&2; + kill $PPID +done < /dev/stdin diff --git a/azure-vote/supervisord-debian.conf b/azure-vote/supervisord-debian.conf new file mode 100644 index 0000000000..ffb4e56839 --- /dev/null +++ b/azure-vote/supervisord-debian.conf @@ -0,0 +1,26 @@ +[supervisord] +nodaemon=true + +[program:uwsgi] +command=/usr/local/bin/uwsgi --ini /etc/uwsgi/uwsgi.ini +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +startsecs = 0 +autorestart=false + +[program:nginx] +command=/usr/sbin/nginx +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +# Graceful stop, see http://nginx.org/en/docs/control.html +stopsignal=QUIT +startsecs = 0 +autorestart=false + +[eventlistener:quit_on_failure] +events=PROCESS_STATE_STOPPED,PROCESS_STATE_EXITED,PROCESS_STATE_FATAL +command=/etc/supervisor/stop-supervisor.sh diff --git a/azure-vote/uwsgi-nginx-entrypoint.sh b/azure-vote/uwsgi-nginx-entrypoint.sh new file mode 100644 index 0000000000..595127807c --- /dev/null +++ b/azure-vote/uwsgi-nginx-entrypoint.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env sh +set -e + +# Get the maximum upload file size for Nginx, default to 0: unlimited +USE_NGINX_MAX_UPLOAD=${NGINX_MAX_UPLOAD:-0} + +# Get the number of workers for Nginx, default to 1 +USE_NGINX_WORKER_PROCESSES=${NGINX_WORKER_PROCESSES:-1} + +# Set the max number of connections per worker for Nginx, if requested +# Cannot exceed worker_rlimit_nofile, see NGINX_WORKER_OPEN_FILES below +NGINX_WORKER_CONNECTIONS=${NGINX_WORKER_CONNECTIONS:-1024} + +# Get the listen port for Nginx, default to 80 +USE_LISTEN_PORT=${LISTEN_PORT:-80} + +if [ -f /app/nginx.conf ]; then + cp /app/nginx.conf /etc/nginx/nginx.conf +else + content='user nginx;\n' + # Set the number of worker processes in Nginx + content=$content"worker_processes ${USE_NGINX_WORKER_PROCESSES};\n" + content=$content'error_log /var/log/nginx/error.log warn;\n' + content=$content'pid /var/run/nginx.pid;\n' + content=$content'events {\n' + content=$content" worker_connections ${NGINX_WORKER_CONNECTIONS};\n" + content=$content'}\n' + content=$content'http {\n' + content=$content' include /etc/nginx/mime.types;\n' + content=$content' default_type application/octet-stream;\n' + content=$content' log_format main '"'\$remote_addr - \$remote_user [\$time_local] \"\$request\" '\n" + content=$content' '"'\$status \$body_bytes_sent \"\$http_referer\" '\n" + content=$content' '"'\"\$http_user_agent\" \"\$http_x_forwarded_for\"';\n" + content=$content' access_log /var/log/nginx/access.log main;\n' + content=$content' sendfile on;\n' + content=$content' keepalive_timeout 65;\n' + content=$content' include /etc/nginx/conf.d/*.conf;\n' + content=$content'}\n' + content=$content'daemon off;\n' + # Set the max number of open file descriptors for Nginx workers, if requested + if [ -n "${NGINX_WORKER_OPEN_FILES}" ] ; then + content=$content"worker_rlimit_nofile ${NGINX_WORKER_OPEN_FILES};\n" + fi + # Save generated /etc/nginx/nginx.conf + printf "$content" > /etc/nginx/nginx.conf + + content_server='server {\n' + content_server=$content_server" listen ${USE_LISTEN_PORT};\n" + content_server=$content_server' location / {\n' + content_server=$content_server' include uwsgi_params;\n' + content_server=$content_server' uwsgi_pass unix:///tmp/uwsgi.sock;\n' + content_server=$content_server' }\n' + content_server=$content_server'}\n' + # Save generated server /etc/nginx/conf.d/nginx.conf + printf "$content_server" > /etc/nginx/conf.d/nginx.conf + + # Generate Nginx config for maximum upload file size + printf "client_max_body_size $USE_NGINX_MAX_UPLOAD;\n" > /etc/nginx/conf.d/upload.conf + + # Remove default Nginx config from Alpine + printf "" > /etc/nginx/conf.d/default.conf +fi + +# For Alpine: +# Explicitly add installed Python packages and uWSGI Python packages to PYTHONPATH +# Otherwise uWSGI can't import Flask +if [ -n "$ALPINEPYTHON" ] ; then + export PYTHONPATH=$PYTHONPATH:/usr/local/lib/$ALPINEPYTHON/site-packages:/usr/lib/$ALPINEPYTHON/site-packages +fi +exec "$@" diff --git a/azure-vote/uwsgi.ini b/azure-vote/uwsgi.ini new file mode 100644 index 0000000000..cb4a893e0f --- /dev/null +++ b/azure-vote/uwsgi.ini @@ -0,0 +1,10 @@ +[uwsgi] +socket = /tmp/uwsgi.sock +chown-socket = nginx:nginx +chmod-socket = 664 +# Graceful shutdown on SIGTERM, see https://github.com/unbit/uwsgi/issues/849#issuecomment-118869386 +hook-master-start = unix_signal:15 gracefully_kill_them_all +need-app = true +die-on-term = true +# For debugging and testing +show-config = true diff --git a/docker-compose.yaml b/docker-compose.yaml index 1e868d2ebc..88c85a42a1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,7 @@ version: '3' services: azure-vote-back: - image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + image: redis:latest container_name: azure-vote-back environment: ALLOW_EMPTY_PASSWORD: "yes" @@ -10,7 +10,7 @@ services: azure-vote-front: build: ./azure-vote - image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 + image: azure-vote-front:v1 container_name: azure-vote-front environment: REDIS: azure-vote-back