├── .dockerignore ├── .github └── workflows │ └── blank.yml ├── Dockerfile ├── README.Docker.md ├── counter-service.py ├── docker-compose.yaml ├── requirements.txt └── sonar-project.properties /.dockerignore: -------------------------------------------------------------------------------- 1 | # Include any files or directories that you don't want to be copied to your 2 | # container here (e.g., local build artifacts, temporary files, etc.). 3 | # 4 | # For more help, visit the .dockerignore file reference guide at 5 | # https://docs.docker.com/go/build-context-dockerignore/ 6 | 7 | **/.DS_Store 8 | **/__pycache__ 9 | **/.venv 10 | **/.classpath 11 | **/.dockerignore 12 | **/.env 13 | **/.git 14 | **/.gitignore 15 | **/.project 16 | **/.settings 17 | **/.toolstarget 18 | **/.vs 19 | **/.vscode 20 | **/*.*proj.user 21 | **/*.dbmdl 22 | **/*.jfm 23 | **/bin 24 | **/charts 25 | **/docker-compose* 26 | **/compose* 27 | **/Dockerfile* 28 | **/node_modules 29 | **/npm-debug.log 30 | **/obj 31 | **/secrets.dev.yaml 32 | **/values.dev.yaml 33 | LICENSE 34 | README.md 35 | -------------------------------------------------------------------------------- /.github/workflows/blank.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker image to AWS ECR 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-and-push: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out the repo 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 # Necessary to fetch all tags and history 17 | 18 | ################################################################ 19 | ### SONAR CLOUD SCAN ### 20 | ### Drops the build if any bugs or vulnerabilities are found.### 21 | ### Using the default quality gate. ### 22 | ### Connected to my personal Sonar Cloud account ### 23 | ################################################################ 24 | 25 | - name: SonarCloud Scan 26 | uses: SonarSource/sonarcloud-github-action@master 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 30 | 31 | - name: Setup Git 32 | run: | 33 | git config --global user.name 'github-actions' 34 | git config --global user.email 'github-actions@github.com' 35 | ################################################################ 36 | ### DETERMINE NEXT VERSION ### 37 | ### Used for creating new releases and image tags ### 38 | ################################################################ 39 | 40 | - name: Determine Next Version 41 | id: next_version 42 | run: | 43 | # Fetch all tags 44 | git fetch --tags 45 | 46 | # Get the latest tag, assume semver, and sort. 47 | LATEST_TAG=$(git tag -l | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1) 48 | 49 | # If there's no tag yet, start with v0.0.0. Used for new repos 50 | if [ -z "$LATEST_TAG" ]; then 51 | LATEST_TAG="v0.0.0" 52 | fi 53 | 54 | # Increment the patch version 55 | NEXT_TAG=$(echo $LATEST_TAG | awk -F. '{print $1"."$2"."$3+1}') 56 | 57 | # Output the next version 58 | echo "::set-output name=tag::$NEXT_TAG" 59 | echo "Next version: $NEXT_TAG" 60 | 61 | ################################################################ 62 | ### CREATE RELEASE ### 63 | ### Creating release with the tag from the previous step ### 64 | ################################################################ 65 | 66 | - name: Create Release 67 | id: create_release 68 | uses: actions/create-release@v1 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN_2 }} 71 | with: 72 | tag_name: ${{ steps.next_version.outputs.tag }} 73 | release_name: Release ${{ steps.next_version.outputs.tag }} 74 | draft: false 75 | prerelease: false 76 | 77 | ################################################################ 78 | ### BUILD DOCKER IMAGE ### 79 | ### Build Docker image from the Dockefile ### 80 | ################################################################ 81 | 82 | - name: Configure AWS credentials 83 | uses: aws-actions/configure-aws-credentials@v1 84 | with: 85 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 86 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 87 | aws-region: ap-south-1 88 | 89 | - name: Login to Amazon ECR 90 | id: login-ecr 91 | uses: aws-actions/amazon-ecr-login@v1 92 | 93 | - name: Extract repository name 94 | id: repo-name 95 | run: | 96 | REPO_NAME="${GITHUB_REPOSITORY##*/}" 97 | echo "REPO_NAME=$REPO_NAME" >> $GITHUB_ENV 98 | echo "::set-output name=repo_name::$REPO_NAME" 99 | 100 | - name: Build Docker image 101 | env: 102 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 103 | ECR_REPOSITORY: ${{ env.REPO_NAME }} 104 | IMAGE_TAG: ${{ steps.next_version.outputs.tag }} 105 | run: | 106 | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 107 | echo "IMAGE_NAME=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_ENV 108 | 109 | - name: Run Snyk to check Docker image for vulnerabilities 110 | uses: snyk/actions/docker@master 111 | env: 112 | SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} 113 | with: 114 | image: ${{ env.IMAGE_NAME }} 115 | args: --severity-threshold=high --policy-path=.snyk 116 | continue-on-error: false 117 | 118 | - name: Push Docker image to Amazon ECR 119 | env: 120 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 121 | ECR_REPOSITORY: python-devops 122 | IMAGE_TAG: ${{ steps.next_version.outputs.tag }} 123 | run: | 124 | # Tag the image as latest 125 | docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest 126 | # Push the specific version tag 127 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 128 | # Push the latest tag 129 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest 130 | 131 | - name: Deploy to EC2 132 | env: 133 | EC2_PEM_KEY: ${{ secrets.EC2_PEM_KEY }} 134 | EC2_HOST: ${{ secrets.EC2_HOST }} 135 | EC2_USER: ${{ secrets.EC2_USER }} 136 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 137 | ECR_REPOSITORY: python-devops 138 | IMAGE_TAG: ${{ steps.next_version.outputs.tag }} 139 | run: | 140 | # Save PEM key to file and set permissions 141 | echo "$EC2_PEM_KEY" > ec2.pem 142 | chmod 400 ec2.pem 143 | 144 | # SSH, SCP commands 145 | SSH_COMMAND="ssh -i ec2.pem -o StrictHostKeyChecking=no $EC2_USER@$EC2_HOST" 146 | SCP_COMMAND="scp -i ec2.pem -o StrictHostKeyChecking=no" 147 | 148 | #Login to the Docker Registry (ECR) 149 | $SSH_COMMAND "aws ecr get-login-password --region ap-south-1 | docker login --username AWS --password-stdin $ECR_REGISTRY" 150 | 151 | # Copy docker-compose.yml to EC2 server 152 | $SCP_COMMAND docker-compose.yml $EC2_USER@$EC2_HOST:/home/centos/docker/ 153 | 154 | # Pull and run the Docker container on EC2 155 | $SSH_COMMAND "cd /home/ubuntu/docker/ && docker pull $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG && docker compose -f docker-compose.yml up -d --force-recreate" 156 | 157 | # Cleanup PEM key 158 | rm -f ec2.pem 159 | 160 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Comments are provided throughout this file to help you get started. 4 | # If you need more help, visit the Dockerfile reference guide at 5 | # https://docs.docker.com/go/dockerfile-reference/ 6 | 7 | # Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 8 | 9 | ARG PYTHON_VERSION=3.13.0a4 10 | FROM python:${PYTHON_VERSION}-alpine3.19 as base 11 | 12 | # Prevents Python from writing pyc files. 13 | ENV PYTHONDONTWRITEBYTECODE=1 14 | 15 | # Keeps Python from buffering stdout and stderr to avoid situations where 16 | # the application crashes without emitting any logs due to buffering. 17 | ENV PYTHONUNBUFFERED=1 18 | 19 | WORKDIR /app 20 | 21 | # Create a non-privileged user that the app will run under. 22 | # See https://docs.docker.com/go/dockerfile-user-best-practices/ 23 | ARG UID=10001 24 | RUN adduser \ 25 | --disabled-password \ 26 | --gecos "" \ 27 | --home "/nonexistent" \ 28 | --shell "/sbin/nologin" \ 29 | --no-create-home \ 30 | --uid "${UID}" \ 31 | appuser 32 | 33 | # Download dependencies as a separate step to take advantage of Docker's caching. 34 | # Leverage a cache mount to /root/.cache/pip to speed up subsequent builds. 35 | # Leverage a bind mount to requirements.txt to avoid having to copy them into 36 | # into this layer. 37 | RUN --mount=type=cache,target=/root/.cache/pip \ 38 | --mount=type=bind,source=requirements.txt,target=requirements.txt \ 39 | python -m pip install -r requirements.txt 40 | 41 | # Switch to the non-privileged user to run the application. 42 | USER appuser 43 | 44 | # Copy the source code into the container. 45 | COPY counter-service.py . 46 | 47 | # Expose the port that the application listens on. 48 | EXPOSE 8080 49 | 50 | # Run the application. 51 | CMD ["gunicorn", "counter-service:app", "--bind", "0.0.0.0:8080", "--access-logfile", "-", "--error-logfile", "-"] -------------------------------------------------------------------------------- /README.Docker.md: -------------------------------------------------------------------------------- 1 | ### Building and running your application 2 | 3 | When you're ready, start your application by running: 4 | `docker compose up --build`. 5 | 6 | Your application will be available at http://localhost:8000. 7 | 8 | ### Deploying your application to the cloud 9 | 10 | First, build your image, e.g.: `docker build -t myapp .`. 11 | If your cloud uses a different CPU architecture than your development 12 | machine (e.g., you are on a Mac M1 and your cloud provider is amd64), 13 | you'll want to build the image for that platform, e.g.: 14 | `docker build --platform=linux/amd64 -t myapp .`. 15 | 16 | Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. 17 | 18 | Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) 19 | docs for more detail on building and pushing. 20 | 21 | ### References 22 | * [Docker's Python guide](https://docs.docker.com/language/python/) -------------------------------------------------------------------------------- /counter-service.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify 2 | import os 3 | 4 | app = Flask(__name__) 5 | 6 | # Define the path for the counter file to store the data in Docker Volume 7 | COUNTER_FILE = "/data/counter.txt" 8 | 9 | def read_counter(): 10 | """ 11 | Reads and returns the current counter value from the file. 12 | If the file doesn't exist, it return 0. 13 | 14 | Returns: 15 | int: The current counter value. 16 | """ 17 | if os.path.exists(COUNTER_FILE): 18 | with open(COUNTER_FILE, "r") as f: 19 | return int(f.read().strip()) 20 | else: 21 | return 0 22 | 23 | def update_counter(counter): 24 | """ 25 | Updates the counter file with the new counter value. 26 | 27 | Args: 28 | counter (int): The new counter value to write to the file. 29 | """ 30 | with open(COUNTER_FILE, "w") as f: 31 | f.write(str(counter)) 32 | 33 | @app.route('/', methods=['GET', 'POST']) 34 | def handle_request(): 35 | """ 36 | Handles GET and POST requests to the root endpoint. 37 | GET request returns the current count of POST requests. 38 | POST request increments the counter and returns the updated count. 39 | 40 | Returns: 41 | str: The response message with the current or updated counter. 42 | """ 43 | counter = read_counter() 44 | if request.method == 'POST': 45 | # Increment the counter for each POST request and update the file. 46 | counter += 1 47 | update_counter(counter) 48 | return f"POST requests counter updated. Current count: {counter}" 49 | else: 50 | # For GET requests, just return the current count. 51 | return f"Current POST requests count: {counter}" 52 | 53 | @app.route('/health', methods=['GET']) 54 | def health_check(): 55 | """ 56 | Performs a simple health check of the application. 57 | It tries to read the counter file as a basic check. 58 | 59 | Returns: 60 | tuple: A JSON response indicating the health status and the HTTP status code. 61 | """ 62 | try: 63 | # Basic health check: Ensure the counter file is accessible. 64 | read_counter() 65 | return jsonify({"status": "healthy"}), 200 66 | except Exception as e: 67 | # Return an unhealthy status if any error occurs, e.g., file access issues. 68 | return jsonify({"status": "unhealthy", "reason": str(e)}), 500 69 | 70 | if __name__ == '__main__': 71 | # Run the Flask app with binding to all interfaces on port 8080. 72 | # Debug mode is turned off for production use. 73 | app.run(host='0.0.0.0', port=8000, debug=True) 74 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '2.4' # The last version of Docker Compose file format that directly supports mem_limit and cpus 2 | services: 3 | counter-service: 4 | container_name: counter-service-exercise 5 | image: 851725297111.dkr.ecr.ap-south-1.amazonaws.com/python-devops:latest 6 | volumes: 7 | - ./data:/data 8 | ports: 9 | - "80:8080" 10 | restart: always 11 | mem_limit: 256M 12 | cpus: 0.5 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.2 2 | gunicorn==21.2.0 -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=profile555_temp 2 | sonar.organization=profile555 3 | 4 | # This is the name and version displayed in the SonarCloud UI. 5 | #sonar.projectName=Temp 6 | #sonar.projectVersion=1.0 7 | 8 | 9 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 10 | #sonar.sources=. 11 | 12 | # Encoding of the source code. Default is default system encoding 13 | #sonar.sourceEncoding=UTF-8 14 | --------------------------------------------------------------------------------