├── .github └── workflows │ └── backend.yml ├── .gitignore ├── README.md ├── backend ├── main.py ├── my-cloud-function.zip └── requirements.txt ├── cloudbuild.yaml ├── media ├── CloudCDN.cach. invalidation.png ├── test_functions.png └── workflow.CI.CD.png ├── terraform ├── main.tf ├── provider.tf ├── terraform.tfvars └── variables.tf └── tests ├── test_functions.py └── test_main.py /.github/workflows/backend.yml: -------------------------------------------------------------------------------- 1 | # Name of the workflow 2 | name: Backend CI/CD Pipeline 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | Test: 11 | name: Static Code Analysis 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v2 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.x' 22 | 23 | - name: Pylint Tests 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install -r backend/requirements.txt 27 | pylint backend/*.py 28 | 29 | Deploy: 30 | name: Infra deployment 31 | runs-on: ubuntu-latest 32 | needs: Test 33 | steps: 34 | - name: Checkout Code 35 | uses: actions/checkout@v2 36 | 37 | 38 | - name: Install zip 39 | run: sudo apt-get install -y zip 40 | 41 | - name: Prepare function deployment 42 | id: prep 43 | run: | 44 | pushd backend 45 | zip -r main.zip . 46 | file_name="main_$(md5sum main.zip | awk '{print $1}').zip" 47 | echo "file_name=${file_name}" >> $GITHUB_ENV 48 | mv main.zip "${file_name}" 49 | popd 50 | 51 | - name: Upload to GCS 52 | env: 53 | GCP_CREDENTIALS: ${{ secrets.GCP_CREDENTIALS }} 54 | run: | 55 | echo "${GCP_CREDENTIALS}" | gcloud auth activate-service-account --key-file=- 56 | gsutil cp backend/${{ env.file_name }} gs://cloudcv_function_vulcu 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | db.sqlite3-journal 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | doc/build/ 72 | doc/source/gen/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # pyenv 81 | .python-version 82 | 83 | # pipenv 84 | #Pipfile.lock 85 | 86 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 87 | __pypackages__/ 88 | 89 | # Celery stuff 90 | celerybeat-schedule 91 | celerybeat.pid 92 | 93 | # SageMath parsed files 94 | *.sage.py 95 | 96 | # Environments 97 | .env 98 | .venv 99 | env/ 100 | venv/ 101 | ENV/ 102 | env.bak/ 103 | venv.bak/ 104 | 105 | # Spyder project settings 106 | .spyderproject 107 | .spyproject 108 | 109 | # Rope project settings 110 | .ropeproject 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | .dmypy.json 118 | dmypy.json 119 | 120 | # Pyre type checker 121 | .pyre/ 122 | 123 | # pytype static type analyzer 124 | .pytype/ 125 | 126 | # Cython debug symbols 127 | cython_debug/ 128 | 129 | # Terraform 130 | .terraform/ 131 | *.tfstate 132 | *.tfstate.* 133 | .terraform.lock.hcl 134 | 135 | # VSCode 136 | .vscode/ 137 | 138 | # Test reports 139 | reports/ 140 | 141 | # Frontend: Node 142 | node_modules/ 143 | npm-debug.log 144 | yarn-error.log 145 | yarn-debug.log 146 | 147 | # Frontend: Bower 148 | bower_components/ 149 | 150 | # Frontend: Misc 151 | .DS_Store 152 | Thumbs.db 153 | *.log 154 | *.csv 155 | *.sublime-project 156 | *.sublime-workspace 157 | .idea/ 158 | *.swp 159 | *.swo 160 | *.sass-cache 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Cloud Platform (GCP) Resume Challenge 2 | 3 | Welcome to my GCP Resume Challenge project! This is a serverless resume, showcasing my skills and experiences in cloud technologies, particularly within the Google Cloud Platform. Here, I'll take you through what I did, how I did it, and why I made certain decisions both in terms of what to do and how to execute it. 4 | 5 | ## Overview 6 | 7 | This project involves the creation of a serverless, dynamic resume hosted on GCP. It's not just a static display of my professional journey, but also a testament to my skills in cloud architecture, serverless technologies, and modern web development practices. 8 | 9 | ## What I Did 10 | 11 | 1. **Created a Static Web Resume**: 12 | - Designed and developed a resume in HTML/CSS, showcasing my professional background. 13 | - Integrated JavaScript to add interactive elements like a visitor counter. 14 | 15 | 2. **Implemented Serverless Backend**: 16 | - Used Google Cloud Functions to handle backend processes such as retrieving and updating the visitor count. 17 | 18 | 3. **Data Storage with Firestore**: 19 | - Chose Google Cloud's Firestore for storing and managing the visitor count data. 20 | 21 | 4. **Infrastructure as Code**: 22 | - Utilized Terraform for defining and deploying all the cloud infrastructure in a codified and version-controlled manner. 23 | 24 | 5. **CI/CD Pipeline**: 25 | - Configured GitHub and Google Cloud Build for continuous integration and deployment, ensuring updates and changes are automatically and safely deployed to production. 26 | 27 | ## How I Did It 28 | 29 | ### Frontend Development 30 | 31 | - **HTML/CSS**: Crafted a clean, responsive design to present my resume. 32 | - **JavaScript**: Wrote a script for dynamically displaying the visitor count. 33 | 34 | ### Backend and Cloud Functions 35 | 36 | - **Python**: Chose Python for Cloud Functions due to its simplicity and efficiency. 37 | - **Firestore Integration**: Used Google Cloud Client Libraries in Python for interacting with Firestore. 38 | 39 | ### Terraform for IaC 40 | 41 | - Defined the entire cloud infrastructure needed for this project in Terraform, including Cloud Functions, Firestore, and necessary permissions. 42 | 43 | ### CI/CD Setup 44 | 45 | - Integrated GitHub with Google Cloud Build to automate the testing and deployment process. 46 | 47 | ## Why I Did It 48 | 49 | ### Technology Choices 50 | 51 | - **GCP and Serverless**: Opted for GCP to showcase my expertise in this cloud platform and serverless to demonstrate the ability to build scalable, efficient applications. 52 | - **Terraform**: Chose IaC for its ability to manage infrastructure in a reproducible way, making the deployment process transparent and efficient. 53 | - **Python**: Selected for backend logic due to its wide acceptance and ease of use in cloud environments. 54 | 55 | ### Design and Implementation Decisions 56 | 57 | - **Static Site with Dynamic Elements**: This approach balances simplicity with interactivity, ensuring the site is easy to host and manage while still being engaging. 58 | - **Firestore for Real-Time Data**: Enables real-time visitor count updates, showcasing real-time data handling capabilities. 59 | 60 | ## Conclusion 61 | 62 | This project is more than a digital resume; it's a reflection of my skills in cloud-based development and my understanding of modern web technologies. It demonstrates my ability to leverage GCP's serverless architecture to build a scalable, responsive web application. 63 | 64 | Feel free to explore the site and see how the visitor count changes with each visit, which is a small yet powerful demonstration of real-time data processing and serverless backend capabilities. 65 | 66 | --- 67 | 68 | Thank you for taking the time to explore my GCP Resume Challenge project! 69 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | """This module handles Firestore operations and Flask request processing.""" 2 | 3 | import logging 4 | from typing import Tuple 5 | from google.cloud import firestore 6 | from flask import jsonify, Request 7 | 8 | # Initialize logger 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | 12 | # pylint: disable=no-member 13 | def get_current_number(number_ref: firestore.DocumentReference) -> int: 14 | """Retrieves the current visitor number from Firestore.""" 15 | try: 16 | doc = number_ref.get() 17 | if doc.exists: 18 | return int(doc.to_dict().get('count', 0)) 19 | return 0 # Return 0 if document does not exist 20 | except firestore.exceptions.NotFound: 21 | logging.error("Firestore document not found") 22 | return 0 # Return 0 or a suitable default value 23 | except firestore.exceptions.FirestoreError as e: 24 | logging.error("Error retrieving visitor number: %s", e) 25 | raise 26 | 27 | def save_visitor_count(number_ref: firestore.DocumentReference, current_number: int) -> None: 28 | """Updates the visitor count in Firestore.""" 29 | try: 30 | number_ref.set({'count': current_number}) 31 | except firestore.exceptions.FirestoreError as e: 32 | logging.error("Error saving visitor count: %s", e) 33 | raise 34 | 35 | def current_number_visitors(_: Request) -> Tuple[dict, int, dict]: 36 | """Handles the incoming request to get and update the current number of visitors.""" 37 | try: 38 | db = firestore.Client() 39 | number_ref = db.collection('visitors').document('visitors_id') 40 | 41 | current_number = get_current_number(number_ref) 42 | new_number = current_number + 1 43 | save_visitor_count(number_ref, new_number) 44 | 45 | data = {'new_number': new_number} 46 | headers = {'Access-Control-Allow-Origin': '*'} 47 | 48 | return jsonify(data), 200, headers 49 | except firestore.exceptions.FirestoreError as e: 50 | logging.error("Error in current_number_visitors: %s", e) 51 | return jsonify({'error': 'An internal error occurred'}), 500 52 | -------------------------------------------------------------------------------- /backend/my-cloud-function.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvulcu/backend_CVcloud/c2bf18a70525629e7ec4c6ff8e1902a84696e69b/backend/my-cloud-function.zip -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | google-cloud-firestore 2 | flask<3.0,>=1.0 3 | pylint 4 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | # Step 1: Run the tests 3 | - name: 'gcr.io/cloud-builders/python' 4 | args: ['pytest', '-v'] 5 | # Step 2: Deploy to Google Cloud Functions 6 | - name: 'gcr.io/cloud-builders/gcloud' 7 | args: ['functions', 'deploy', 'getCount', '--trigger-http', '--runtime', 'python39', '--allow-unauthenticated'] 8 | -------------------------------------------------------------------------------- /media/CloudCDN.cach. invalidation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvulcu/backend_CVcloud/c2bf18a70525629e7ec4c6ff8e1902a84696e69b/media/CloudCDN.cach. invalidation.png -------------------------------------------------------------------------------- /media/test_functions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvulcu/backend_CVcloud/c2bf18a70525629e7ec4c6ff8e1902a84696e69b/media/test_functions.png -------------------------------------------------------------------------------- /media/workflow.CI.CD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mvulcu/backend_CVcloud/c2bf18a70525629e7ec4c6ff8e1902a84696e69b/media/workflow.CI.CD.png -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | 2 | # Cloud Storage bucket for Cloud Function code 3 | resource "google_storage_bucket" "cloud_function_bucket" { 4 | name = "${var.bucket_name}_vulcu" 5 | location = var.region 6 | force_destroy = true 7 | storage_class = "REGIONAL" 8 | } 9 | 10 | # Cloud Function 11 | resource "google_cloudfunctions_function" "current_number_visitors" { 12 | name = "current_number_visitors" 13 | runtime = "python39" 14 | available_memory_mb = 128 15 | timeout = 60 16 | entry_point = "current_number_visitors" 17 | source_archive_bucket = google_storage_bucket.cloud_function_bucket.name 18 | source_archive_object = var.zip_file 19 | trigger_http = true 20 | project = var.project 21 | region = var.region 22 | } 23 | 24 | # Public access to the Cloud Function 25 | resource "google_cloudfunctions_function_iam_member" "public_access_current_number_visitors" { 26 | project = var.project 27 | region = var.region 28 | cloud_function = google_cloudfunctions_function.current_number_visitors.name 29 | role = "roles/cloudfunctions.invoker" 30 | member = "allUsers" 31 | } 32 | -------------------------------------------------------------------------------- /terraform/provider.tf: -------------------------------------------------------------------------------- 1 | provider "google" { 2 | credentials = var.gcp_credentials 3 | project = var.project 4 | region = var.region 5 | } 6 | -------------------------------------------------------------------------------- /terraform/terraform.tfvars: -------------------------------------------------------------------------------- 1 | project = "crafty-campaign-401215" 2 | region = "us-central1" 3 | # gcp_credentials = "<>" 4 | bucket_name = "cloudcv_function" 5 | zip_file = "my-cloud-function.zip" -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project" { 2 | description = "The Google Cloud project ID" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "The Google Cloud region" 8 | type = string 9 | } 10 | 11 | variable "gcp_credentials" { 12 | description = "Google Cloud credentials in JSON format" 13 | type = string 14 | } 15 | 16 | variable "bucket_name" { 17 | description = "Name of the Google Cloud Storage bucket" 18 | type = string 19 | default = "cloudcv_function" 20 | } 21 | 22 | variable "zip_file" { 23 | description = "Name of the ZIP file containing the Cloud Function" 24 | type = string 25 | } 26 | -------------------------------------------------------------------------------- /tests/test_functions.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('../Backend') 3 | import pytest 4 | from main import get_current_number 5 | from unittest.mock import Mock 6 | 7 | def test_get_current_number(): 8 | # Create a moc for the Firestore document 9 | mock_doc = Mock() 10 | mock_doc.exists = True 11 | mock_doc.to_dict.return_value = {'count': 5} 12 | 13 | # Create a moc for the Firestore client 14 | mock_db = Mock() 15 | mock_db.collection().document().get.return_value = mock_doc 16 | 17 | # Calling a function with mocked up data 18 | assert get_current_number(mock_db, mock_db.collection().document()) == 5 19 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from main import current_number_visitors 3 | from unittest.mock import Mock, patch 4 | 5 | @pytest.fixture 6 | def app(): 7 | # An instance of the Flask application is created here with the TESTING=True flag 8 | # to enable testing mode 9 | from your_flask_app import create_app 10 | app = create_app({'TESTING': True}) 11 | return app 12 | 13 | def test_error_handling(app): 14 | with app.app_context(): 15 | with patch('main.firestore.Client') as mock_client: 16 | # Simulate a scenario where Firestore raises an exception 17 | mock_client.side_effect = Exception("Connection error") 18 | 19 | # Check that the function correctly handles the exception 20 | response = current_number_visitors(Mock()) 21 | assert response.status_code == 500 # or any other expected error code 22 | assert 'error' in response.json 23 | --------------------------------------------------------------------------------