diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml new file mode 100644 index 00000000..d28ee18e --- /dev/null +++ b/.github/workflows/ci-pr.yml @@ -0,0 +1,34 @@ +on: + pull_request: # Ce workflow se déclenche uniquement sur les MR + +jobs: + ci_merge_request: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Backend + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Build backend + run: dotnet build --configuration Release + working-directory: ./backend + + # Frontend + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: '21.7.1' + + - name: Build frontend + run: npm install && npm run build + working-directory: ./frontend + + - name: Lint frontend + run: npm run lint + working-directory: ./frontend diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..2d08e956 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,100 @@ +# -------------------------- +# Pipeline CI +# Déclenche uniquement sur push d'un tag vMAJOR.MINOR.PATCH +# -------------------------- +name: "ci_pipeline" +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + +jobs: + # -------------------------- + # CI Frontend : build & artifact + # -------------------------- + ci_frontend: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '21.7.1' + + - name: Install & Build Frontend + run: npm install && npm run build + env: + VITE_APP_VERSION: ${{ github.ref_name }} + working-directory: ./frontend + + - name: Upload Frontend Artifact + uses: actions/upload-artifact@v4 + with: + name: frontend-dist + path: ./frontend/dist + retention-days: 2 + + package_version: + runs-on: ubuntu-latest + permissions: + contents: write + needs: [ci_frontend] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '21.7.1' + + # 1. On change la version dans le fichier package.json localement + - name: Update package.json version + working-directory: ./frontend + run: npm version ${{ github.ref_name }} --no-git-tag-version + + # 2. On sauvegarde ce changement sur le dépôt GitHub + - name: Commit and Push version update + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add frontend/package.json + # Le [skip ci] est crucial pour ne pas relancer le pipeline en boucle ! + git commit -m "chore: bump version to ${{ github.ref_name }} [skip ci]" + git push origin HEAD:main + + # -------------------------- + # CI Backend : publish & artifact + # -------------------------- + ci_backend: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '9.0.x' + + - name: Publish Backend + run: dotnet publish -c Release -o ./myapp + working-directory: ./backend + + - name: Upload Backend Artifact + uses: actions/upload-artifact@v4 + with: + name: backend-publish + path: ./backend/myapp + retention-days: 2 \ No newline at end of file diff --git a/.github/workflows/deploy-infra-and-apps.yml b/.github/workflows/deploy-infra-and-apps.yml new file mode 100644 index 00000000..487a7fa2 --- /dev/null +++ b/.github/workflows/deploy-infra-and-apps.yml @@ -0,0 +1,137 @@ +# -------------------------- +# Pipeline CD +# Déclenche uniquement sur la complétion du CI pipeline +# -------------------------- +name: "cd_pipeline" +on: + workflow_run: + workflows: ["ci_pipeline"] + types: completed + + +# Variables globales pour le workflow +env: + AZURE_RG_NAME: rg-${{ vars.PROJECT_NAME }}-${{ vars.AZURE_RESOURCE_IDENTIFIER }} + +jobs: + + # -------------------------- + # CD : Déployer l'infrastructure + # -------------------------- + deploy_infrastructure: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + permissions: + id-token: write + contents: read + environment: production + + outputs: + appServiceName: ${{ steps.bicep_deploy.outputs.appServiceName }} + staticWebAppName: ${{ steps.bicep_deploy.outputs.staticWebAppName }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Create resource group if not exists + run: | + az group show --name ${{ env.AZURE_RG_NAME }} || \ + az group create --name ${{ env.AZURE_RG_NAME }} --location ${{ secrets.AZURE_REGION }} + + - name: Deploy Bicep + id: bicep_deploy + uses: azure/arm-deploy@v2 + with: + subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }} + region: ${{ secrets.AZURE_REGION }} + template: ./infrastructure/main.bicep + parameters: project=${{ vars.PROJECT_NAME }} location=${{ secrets.AZURE_REGION }} swaLocation=${{ secrets.AZURE_SWA_REGION }} identifier=${{ vars.AZURE_RESOURCE_IDENTIFIER }} + resourceGroupName: ${{ env.AZURE_RG_NAME }} + + # -------------------------- + # CD Backend : télécharger artifact & déployer + # -------------------------- + deploy_backend: + runs-on: ubuntu-latest + needs: [deploy_infrastructure] + permissions: + id-token: write + contents: read + actions: read + environment: production + steps: + - name: Checkout + uses: actions/checkout@v4 + + + - name: Download Backend Artifact + uses: actions/download-artifact@v4 + with: + name: backend-publish + run-id: ${{ github.event.workflow_run.id }} + path: ./backend-publish + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Deploy Backend to App Service + uses: azure/webapps-deploy@v2 + with: + app-name: ${{ needs.deploy_infrastructure.outputs.appServiceName }} + package: ./backend-publish + + # -------------------------- + # CD Frontend : télécharger artifact & déployer + # -------------------------- + deploy_frontend: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + actions: read + needs: [deploy_infrastructure] + environment: production + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download Frontend Artifact + uses: actions/download-artifact@v4 + with: + name: frontend-dist + run-id: ${{ github.event.workflow_run.id }} + path: ./frontend/dist + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Get Static Web App Deployment Token + run: | + SWA_DEPLOYMENT_TOKEN=$(az staticwebapp secrets list -n ${{ needs.deploy_infrastructure.outputs.staticWebAppName }} -o tsv --query properties.apiKey) + echo "SWA_DEPLOYMENT_TOKEN=$SWA_DEPLOYMENT_TOKEN" >> $GITHUB_ENV + + - name: Deploy Frontend to Static Web App + uses: azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ env.SWA_DEPLOYMENT_TOKEN }} + app_location: ./frontend/dist + action: upload + skip_app_build: true + skip_api_build: true \ No newline at end of file diff --git a/README.md b/README.md index 735e2405..57640204 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,2 @@ # Description -`ParkNDeploy` is an introductory DevOps course designed to guide you through deploying a basic Parking Finder App on Azure. - -It follows [GitRDone](https://github.com/jraillard/gitrdone), a hands-on course to get you up to speed with version control using Git. - -This course covers continuous integration and continuous deployment (CI/CD) pipelines, as well as infrastructure-as-code (IaC) practices. - -## Prerequisites - -### Tools - -- An Azure Account in order to deploy your App :rocket: - - [Azure Students](./doc/azure_students.md) - - [Classic one](https://azure.microsoft.com/pricing/purchase-options/azure-account?icid=azurefreeaccount) (you will be ask to put a credit card even if nothing is debited) - -- A GitHub account in order to fork this repo and start to work :wink: - -- A source code management tool : - - Git Bash for CLI guys :sunglasses: - - [Fork](https://git-fork.com/) for GUI guys :star: - -- **[Optional]** IDEs to build the app locally : - - Visual Studio Community with .Net 9 SDK (Backend) - - Visual Studio Code & Node JS >= 21.7.1 (Frontend) - -### Knowledges - -- **[Appreciated]** Basic repository management (commits, push, merge-request) -- **[Optional]** Basic understanding of APIs -- **[Optional]** Basic understanding of SPAs - -## Build the App locally - -[Getting the project](#getting-the-project) is mandatory. -[Backend](#backend) and [Frontend](#frontend) build step will only be mandatory at the end of this workshop. - -### Getting the project - -First of all, you'll need to get the source code :grin: : - -- Fork this project on your personnal GitHub account - -:warning: Keep `Copy the DEFAULT branch only` option checked (to avoid conflicts between branch names). - -> If you never made a fork, just follow the steps mentionned [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo#forking-a-repository) :eyes:. - -- Clone the project on your local machine - -> Again, if you never did it, just follow the steps mentionned [here](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo#cloning-your-forked-repository) :eyes:. - -- And that's it ! :sparkles: - -> :bulb: The repository will contain the `main` branch as you're starting point. -> You're free to use `feature branches` or not. -> -> If for some reasons you're blocked during this workshop, you can check -> `solution-` branches on the main repository.:smirk: - -### Backend ---- -Easiest one : -- Open the `ParkNDeploy.sln` file with Visual Studio -- Hit the `Run` button using the `Project Https` profile (default one) -- Wait the Swagger API to launch on your default navigator -- You can start to play with it to see what it does :video_game: - -> Some details about how the API is made and what it does could be find in the [backend README file](./backend/README.md). - -### Frontend ---- - -Follow the next steps : - -:one: Open the `./frontend` folder with Visual Studio Code - -:two: Open a command line terminal using `CTRL+ù` hotkey or through the `Terminal menu` on the top of Visual Studio Code - -:three: Run the following commands : - -```bash -# This will download all the dependencies for the frontend -npm install - -# This will compiles and run the frontend app under a Vite developpement server -npm run dev - -# If it works, you should see a localhost URL link -``` - -:four: Show the app in browser, here you have two possibilities : - -- Without Visual Studio Code debugger : just `CTRL+Click` on the localhost URL that is being displayed on the terminal you just launched before - -- With Visual Studio Code debugger : - - Hit `CTRL+SHIFT+D` hotkey or click on ![debug icon](./doc/assets/vscode_debug_icon.png) in the left navigation bar - - - Click on ![play button](./doc/assets/vscode_debug_play_button.png) - - → Basically VS Code will run the [launch.json config](./frontend/.vscode/launch.json) which launch a Chrome navigator and attach the VS Code debugger to the frontend app process. This will allow you to debug through breakpoints and so on inside Visual Studio code (instead of spamming your source code with `console.log()` :stuck_out_tongue_winking_eye:). - -> Some details about how the Frontend App is made and what it does could be find in the [frontend README file](./frontend/README.md) - -## Let's dive-In ! - -:rocket: Buckle up, folks! It's time to blast off to the [first step](./doc/step0_studying_devops.md) of our course. Ready, set, deploy! - -## Additional Resources (for DevOps enthusiastics) - -First of all, I hope this initiation gave you interest into DevOps concepts and that you understand how it could help you in your day-to-day tasks in real-world project. - -Now for a treasure trove of additional resources to dive deeper into DevOps concepts, check out the [**to go further section**](doc/to_go_further.md). :eyes: +Deploying a basic Parking Finder App on Azure. diff --git a/doc/assets/app_service_default_page.png b/doc/assets/app_service_default_page.png deleted file mode 100644 index ef5d6ec9..00000000 Binary files a/doc/assets/app_service_default_page.png and /dev/null differ diff --git a/doc/assets/azure_portal.png b/doc/assets/azure_portal.png deleted file mode 100644 index 4ed362e0..00000000 Binary files a/doc/assets/azure_portal.png and /dev/null differ diff --git a/doc/assets/backend_swagger.png b/doc/assets/backend_swagger.png deleted file mode 100644 index 2248dc7d..00000000 Binary files a/doc/assets/backend_swagger.png and /dev/null differ diff --git a/doc/assets/final_deployed_app.png b/doc/assets/final_deployed_app.png deleted file mode 100644 index 6739521e..00000000 Binary files a/doc/assets/final_deployed_app.png and /dev/null differ diff --git a/doc/assets/react_app_no_backend_error.png b/doc/assets/react_app_no_backend_error.png deleted file mode 100644 index d301d74e..00000000 Binary files a/doc/assets/react_app_no_backend_error.png and /dev/null differ diff --git a/doc/assets/swa_default_page.png b/doc/assets/swa_default_page.png deleted file mode 100644 index 3af272c9..00000000 Binary files a/doc/assets/swa_default_page.png and /dev/null differ diff --git a/doc/assets/uai_role_assignment.png b/doc/assets/uai_role_assignment.png deleted file mode 100644 index 2c1c1dde..00000000 Binary files a/doc/assets/uai_role_assignment.png and /dev/null differ diff --git a/doc/assets/vscode_debug_icon.png b/doc/assets/vscode_debug_icon.png deleted file mode 100644 index 596857a8..00000000 Binary files a/doc/assets/vscode_debug_icon.png and /dev/null differ diff --git a/doc/assets/vscode_debug_play_button.png b/doc/assets/vscode_debug_play_button.png deleted file mode 100644 index d9975add..00000000 Binary files a/doc/assets/vscode_debug_play_button.png and /dev/null differ diff --git a/doc/azure_students.md b/doc/azure_students.md deleted file mode 100644 index 9a3144f2..00000000 --- a/doc/azure_students.md +++ /dev/null @@ -1,25 +0,0 @@ -# Azure Students account - -If you are a students, `Azure` make you able to get a free Azure account with some credits without the need of giving your credit card (used as insurance in classic Azure Account if you spend too much computes on Azure). - -In order to create an Azure Students account, follow the next steps : - -:one: Go the [AzureStudents WebSite](https://azure.microsoft.com/free/students/) - -:two: Click on `Start free` - -:three: Put your student email address - -:four: Connect on your school authentication page (if there's one) and follow the steps - -→ You might receive a mail saying that your account has been created or that you should validate it before. - -:five: Therefore you should be able to connect to [Azure Portal](https://portal.azure.com/#home) and have a screen like this : - -![azure_portal](./assets/azure_portal.png) - -:six: Through the search bar, look for `Subscriptions` service - -:seven: There you should have your `Azure for Students` subscription displayed - -Tadah ! :sparkles: \ No newline at end of file diff --git a/doc/step0_studying_devops.md b/doc/step0_studying_devops.md deleted file mode 100644 index 894d9146..00000000 --- a/doc/step0_studying_devops.md +++ /dev/null @@ -1,102 +0,0 @@ -# Preamble : Studying DevOps a bit - -This part aim to describe some technical terms we'll use a lot and express what will be exactly the purpose of this workshop. - -## ***How would you define DevOps ?*** - -→ I kinda like the [Microsoft one](https://azure.microsoft.com/resources/cloud-computing-dictionary/what-is-devops) which says that it's the union from processes, and technologies from developpement (Dev) and operational (Ops) teams in order to accelerate the delivery of high-quality products to their customers. - -That means that `DevOps` is bound to all the application lifecycle : -- `PLAN` : Ideate, define features and track progress and bugs using agile development, kanban boards and KPI dashboard -- `DEVELOP` : Develop code in teams (write, test, review, integration), build artifacts (application packages), use automations (tests, formating, security checks) -- `DELIVER` : Deploy your application consistently and reliably, configure managed infrastructure, be able to deliver frequently -- `OPERATE` : Maintain, control and get feedback on your application through monitoring, alerting and troubleshooting on production environments - -Embrace the `DevOps` is in vogue; it is a proven methodology. :star: - -In this initiation course we will focus on : -- `DEVELOP` : Be able to package both frontend and backend into deployable artifacts -- `DELIVER` : Provision an Azure Infrastructure and deploy your artifacts in it to be always able to find a parking in Angers :sparkles: - - -## Infrastructure as Code - -Infrastructure as Code or `IaC` means that we will be allowed to create our Infrastructure, in Azure for our case, using code. - -There's two ways of doing IaC : -- `imperative` : you'll have to describe each steps to end with the desired infrastructure (think like an algorithm) -- `declarative` : you'll have to defines the desired state and the tool you're using will be responsible of executing needed steps - -> :bulb: Most of the time, you'll looking for declarative instead of imperative as it is more easy to read / maintain and evolve ; but sometimes you'll not be allowed to do such a thing. - -Here's a non-exhaustive list of IaC tools : -- Ansible (imperative, declarative in some ways) -- Terraform (declarative) -- Bicep (declarative) - -## Continuous Integration - -Continuous Integration, or `CI`, is a process that allow developers to gather all their works into a single or multiple deployable artifacts. - -You'll likely hear about ***CI pipelines*** as it could be schematized as a pipe starting with an event (Pull Request, git tag, deployment button, etc.), passing through integration steps : check for the application to still be buildable, running automated tests (unit, integration, security), code formating and ending with one or more packages. - -Packages can be of different shapes : zip files, images, docker images, vm images and should be made in order to be directly (or simply) deployable without any modifications. In that way, CI pipelines are usually followed by `CD pipelines` (Continuous Deploymoyent pipelines). - -Platform that are hosting your code usually offer a way of writing CI/CD pipelines : -- GitHub Actions → GitHub -- Azure Pipelines → Azure DevOps -- GitLab Pipelines → GitLab - -But you can also find some other third-party tools such as : -- Jenkins -- ArgoCD -- BitBucket Pipeline -- RunsOn -- Quirrel -- etc. - -## Continuous Deployment - -Continuous Deployment, or `CD`, is a process that allow to deploy an artifact to an environment (dev, test, stating, production, etc.). - -***CD pipelines*** could be directly triggered at the end of a CI pipeline or you could had some `guards` such as : manual validation, tests on artifact, wait for a schedule, you could think about **almost** anything (time will brake your imagination :stuck_out_tongue:). - -CD pipelines are usually split into two main parts : -- provisionning the environment infrastructure -- deploying our artifact - -... but they can be chained ! :smirk: - -You could think about the following process : - -:one: Developers are pushing their code to the main branch with some nice new features - -:two: An artifact is made by running a CI pipeline on this branch - -:three: A first CD pipeline deploy this package to a QA environment so that UI & Functionnal tests are made - -:four: Testers put their stamp so a second CD pipeline is triggered to a staging environment - -:five: Team is agreeing that our new version is now ready and a last CD pipeline is triggered to push our package to production environment - -Et voilà ! :sparkles: - -> :bulb: CI/CD pipelines conception is hardly-bound to the team / application size and the time-to-deliver a feature : every project could have a unique one. - -## What will we do with ParkNDeploy then ? - -The next steps of this workshop will aim to : -- make you write IaC scripts to host both Frontend and Backend App -- make you write CD pipelines to execute IaC scripts and deploy our apps -- trigger your pipelines each time you push to your remoted branches - -We'll use : -- `Azure` to host our infrastructure (in the cloud :cloud:) -- `GitHub Actions` as CI/CD pipeline tool -- `Bicep` as IaC tool (recommended one to provision Azure resources) - -CI pipelines will voluntary not be covered as we want to focus on deploying : we don't need any artifact checking process or anything else as the ParkNDeploy is already written and ready to go. - -Nevertheless, you'll be suggest to create CI pipeline at the end as bonus exercices to match with much real-world process :smirk:. - -Still seems confusing ? Don't worry, and [let's deploy our backend API](./step1_deploy_backend.md) :sunglasses:. \ No newline at end of file diff --git a/doc/step1_deploy_backend.md b/doc/step1_deploy_backend.md deleted file mode 100644 index 5f3115db..00000000 --- a/doc/step1_deploy_backend.md +++ /dev/null @@ -1,427 +0,0 @@ -# Step 1 : Deploy ParkNDeploy API - -## State about Azure resources - -Before talking about `Bicep`, let's first talk a bit about Azure resources we will provision. :eyes: - -We need to deploy a basic API, the Azure resource that best fit our needs is the [Azure App Service](https://azure.microsoft.com/fr-fr/products/app-service) as it is a simple Saas resource to use in that case. - -An App Service should always be deployed under an `App Service Plan` : think of it like a wrapper, it allows you to do many things such as App Service instances scale or defining [deployment slots](https://learn.microsoft.com/azure/app-service/deploy-staging-slots?tabs=portal) (way too advanced for this course and therefore will not be covered :wink:). - -Finally, in Azure, ***every single resources*** should be created under a `resource group`. Usually we associate it with an environment as it is, by default, ensuring compartmentalization. - -Resource groups are also bound to a subscription (you're Azure Student one for example), also bound to a tenant (in your case, the one bound to your microsoft email adress). - -That being said, our Azure infrastructure will look like this : - -``` -tenant <- no need to be provisionned (but authentication required ...) - └── subscription <- no need to be provisionned (but authentication required ...) - └── resourceGroup - └── appServicePlan - └── appService - └── ** Your Wonderfull Backend ** -``` - -## Create API Infrastructure - -Finally, we'll start writing code ! :star2: - -As we mentionned before, `Bicep` is the tool that will allow us to provision our infrastructure in a declarative way. - -We'll going into a quite straightforward way of doing it but keep in mind that way more complex bicep projects could be made following the [official documentation](https://learn.microsoft.com/azure/azure-resource-manager/bicep/). - -First of all let's create an `infrastructure` folder at the project root that will look like this : - -``` -├── ... -├── infrastructure <- folder dedicated for infrastructure - ├── main.bicep <- bicep project entrypoint - └── modules <- bicep modules (reusable components) - ├── appService.bicep <- bicep module dedicated to provision an Azure App Service - └── appServicePlan.bicep <- bicep module dedicated to provision an Azure App Service Plan -├── ... -``` - -As the previous schema induced, a bicep project should have an entrypoint : the `main.bicep` file. - -It will be responsible of : -- **defining the scope** : [multiple values](https://learn.microsoft.com/azure/azure-resource-manager/bicep/deploy-to-resource-group?tabs=azure-cli) are allowed but we will use `resourceGroup` as everything will be provision in a single resource group. -- **taking some parameters** : variables to help you name your resources for instance -- **create your resource** : through modules or directly -- **export some outputs** : each resources exposes data that can be outputed such as the App Service default host for example - -Add the following code to the `main.bicep` file : - -```bicep -targetScope = 'resourceGroup' // We'll deploy the resources in the provided resource group - -// Parameters to easily construct resource names -param location string -param project string - -// Here we'll add an identifier to create a unique name for the App Service Plan, for example your trigram, so that everyone could deploy his own parkndeploy instance -param identifier string - -// Create the AppServicePlan through the AppServicePlan module -module appServicePlan 'modules/appServicePlan.bicep' = { - name: 'appServicePlan' - params: { - location: location - project: project - identifier: identifier - } -} - -// Create the AppService through the AppService module -module appService 'modules/appService.bicep' = { - name: 'appService' - params: { - location: location - project: project - identifier: identifier - planId: appServicePlan.outputs.planId // Use the appServicePlan output to get its id back => an App Service needs to reference its App Service Plan - } -} - -// Export App Service Name -output appServiceName string = appService.outputs.appServiceName -``` - -Only one more comment on this, `appServiceName` is being outputed as we will need it in [Deploy Backend API part](step1_deploy_backend.md#deploy-backend-api) :wink:. - -Then fill the `./modules/appServicePlan.bicep` file with the following code : - -```bicep -param location string -param project string -param identifier string - -resource plan 'Microsoft.Web/serverfarms@2022-09-01' = { - name: '${project}-plan-${identifier}' - location: location - - sku: { - name: 'F1' // We use F1 pricing plan (free one) as we don't need specific features - } - - kind: 'app,linux' // Allow to deploy on an App Service using Linux OS - - properties: { - reserved: true // Specifity of App Service with Linux OS - } -} - -output planId string = plan.id // Export the App Service identifier -``` - -Pay attention about the `name` property. ***Resources names should be strictly unique in Azure***. As a best practice, every resource name should contain the [resource type abreviation](https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/resource-abbreviations). Some are placing it as prefix, some as suffix and some in middle : there's no restriction on it, it's **just better to be consistent on your different projects** (in order to apply rules on resources inside an organization for instance). - -So for each of our resources we'll follow the following rule : projectName-resourceType-**identifier**, where identifier could be your trigram for instance (as every people in the course should have a unique resource name). - -`location` is interesting too, as Azure is a cloud provider, you'll always have to specify ***where your resource is deployed***. The choice count a lot : -- it could impact resource response time (if too far from where you are) -- every resources are not allowed in every locations -- resources doesnt have the same price depending on where they are located - -Finally, add the code for `./modules/appService.bicep` file : - -```bicep -param location string -param project string -param identifier string - -// App Service Plan identifier that will host our App Service -param planId string - -resource app 'Microsoft.Web/sites@2022-03-01' = { - name: '${project}-app-${identifier}' - location: location - - properties: { - serverFarmId: planId - reserved: true - - siteConfig: { - linuxFxVersion: 'DOTNETCORE|9.0' // Specify to setup the .NET Core 9.0 runtime (used by our backend API) on the Linux machine under the hood - } - } -} - -output appServiceName string = app.name // Export the App Service name for deployment -``` - -Well done friends, our bicep project is ready to be run, let's continue with our CD pipeline. :eyes: - -## Create infrastructure deployment pipeline - -Pipelines in Github are created with GitHub Action tool. - -What a better way to understand the main concepts (workflows, events, jobs, actions) than read the official [getting started first page](https://docs.github.com/actions/about-github-actions/understanding-github-actions) ? :mag: - -Seems clear ? Then checkout the `.github/workflows/deploy-infra-and-apps.yml` file will be using for our entire workshop : - -```yaml -on: [push, workflow_dispatch] - -env: - AZURE_RG_NAME: rg-${{ vars.PROJECT_NAME }}-${{ vars.AZURE_RESOURCE_IDENTIFIER }} - -jobs: - deploy_infrastructure: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Login to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - enable-AzPSSession: true - - - name: Create resource group if not exists - run: | - az group show --name ${{ env.AZURE_RG_NAME }} || - az group create --name ${{ env.AZURE_RG_NAME }} --location ${{ secrets.AZURE_REGION }} - - - name: Deploy bicep - id: bicep_deploy - uses: azure/arm-deploy@v2 - with: - subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }} - region: ${{ secrets.AZURE_REGION }} - template: ./infrastructure/main.bicep - parameters: project=${{ vars.PROJECT_NAME }} location=${{ secrets.AZURE_REGION }} identifier=${{ vars.AZURE_RESOURCE_IDENTIFIER }} - resourceGroupName: ${{ env.AZURE_RG_NAME }} -``` - -Let's take a time to explain the syntax a bit : -- the `on` keyword allows you to defined events - - `push` means every push on any remoted branch will trigger the workflow - - `workflow_dispatch` means that you can trigger manually the workflow on GitHub Action tab on your GitHub repository (only work if the workflow is on the repository default branch or if you trigger it using Github CLI) -- the `env` keyword allows for defining workflow scoped environment variable, here we use it to define the resource group name we'll use multiple times -- `deploy_infrastructure` is our first job responsible of provisionning our Azure infrastructure (as its names implies) - - `runs-on` define the **runner** where our job steps will be executed, [many are available](https://github.com/actions/runner-images) depending on your needs - - each steps have a `name` that will be displayed on workflow run logs to easily follow the execution - - `id` could be specified to identify a step an reference it (for getting outputs for instance ... :smirk:) - - `uses` is used when you want to need to execute a custom action - - you could pass parameters to an action using `with` keyword - - `run` allows you to simply run commands on your runner -- multiple variables are used and [allowed by GitHub actions](https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables): - - `env.xxx` : mentionned before - - `vars.xxx` : organization (on github enterprise instance) / repository / environment scoped variables defined in your `GitHub Settings` under `Secrets and variables / Actions` - - `secrets.xxx` : organization (on github enterprise instance) / repository / environement secrets defined in your `GitHub Settings` under `Secrets and variables / Actions` - - → ***Secrets are masked by default in every logs***, variables could also beeing masked by [applying one](https://docs.github.com/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#masking-a-value-in-a-log). - - - > **Note** : In this workshop we're using GitHub secrets functionnality for simplicity but GitHub also allows you to store them in any third party tool such as Azure Key Vault or HashiCorp Vault for instance. :wink: - -:bulb: If anytime you're looking for workflow syntax, look at the [official documentation](https://docs.github.com/actions/writing-workflows/workflow-syntax-for-github-actions) :eyes:. - -Now talk about actions that will be executed : - -:one: Checkout the repository - -→ Get the source code where the event was triggered on (tag, branch, repository, whatever) - -:two: Login to Azure - -→ Resources will be bound to your tenant & subscription, so we'll need to log in. The `enable-AzPSSession: true` option only allow us to open a PowerShell session in order to runs some az cli command right after - -:three: Create resource group if not exists - -→ Name is clear but why are we doing it ? Basically bicep doesnt allow us to both create resource group and provision resources inside. It is a known limitation and therefore one solution could be to provision it but using `az cli` instead of bicep scripts - -:four: Deploy bicep - -→ Finally, execute our bicep project ! - - -You surely notice that this workflow is using a lot a variables, let's provision it :eyes: : -- Variables : - - **AZURE_RESOURCE_IDENTIFIER** : put your trigram here or whatever a unique id (alphanumerical) - - **PROJECT_NAME** : parkndeploy -- Secrets : - - **AZURE_REGION** : select one of this [list](https://gist.github.com/ausfestivus/04e55c7d80229069bf3bc75870630ec8) depending on when you are (for instance **francecentral** for frenchies) - - → For the following variables, you'll need to follow the [next part](./step1_deploy_backend.md#provision-azure-identity) :clipboard:. - - **AZURE_CLIENT_ID** - - **AZURE_SUBSCRIPTION_ID** - - **AZURE_TENANT_ID** - -## Provision Azure Identity - -To deploy resources on Azure, you need to create an identity that will be allowed to do it. - -There are many [ways](https://learn.microsoft.com/azure/developer/github/connect-from-azure) to do it, we'll use a `user-assigned manged identity` using **OpenID Connect** (OIDC) as it is the recommended way : -- for PoC (proof of concept) situation → more simple to create -- more secured than a basic secret using service principals - -How to do so ? - -:one: Create a user-assigned managed identity following this [doc](https://learn.microsoft.com/entra/identity/managed-identities-azure-resources/how-manage-user-assigned-managed-identities?pivots=identity-mi-methods-azp#create-a-user-assigned-managed-identity) - -→ For the resource group name, name it as : rg-uai-**yourIdentifier** -→ for the uai itself name it : uai-parkndeploy-production-**yourIdentifier** - -:bulb: **rg** for resource group and **uai** for user assigned identity - -:two: Copy the values for Client ID, Subscription ID, and Directory (tenant) ID to use later in your GitHub Actions workflow. - -→ **AZURE_CLIENT_ID & AZURE_SUBSCRIPTION_ID** could be find in your **UAI** ; **AZURE_TENANT_ID** could be find by looking for ***"Tenant properties"*** in the search bar. You can add them as GitHub secrets as you did before with **AZURE_REGION** - -:three: Assign an appropriate role to your user-assigned managed identity by going on its `Azure Role Assignement section` and using its `Add role assignment preview button` - -![Add UAI role](./assets/uai_role_assignment.png) - -→ Set `scope` to **subscriptions** (means you can do things on your subscriptions services) and **contributor** to `role` (meaning you have full access despite Role base access assignement, azure blueprint management and so on => we dont need that at all) - -:five: Configure a federated identity credential on a user-assigned managed identity to trust tokens issued by GitHub Actions to your GitHub repository **under your UAI / Settings / Federated credentials** tab - -→ Here you have some options to set : -- `scenario` : GitHub Actions deploying Azure resources -- `organisation` : your GitHub account storing your forked project -- `repository` : your repo name -- `entity` : environment -- `environment` : production -- `name` : parkdndeploy-gh-fed-cred-env-prod-yourIdentifier (the name should reflect your use case as you might need to create multiple one for the same uai) - -:bulb: `issuer` is auto-generated and allows OIDC to recognize the caller as a GitHub action. `subject identifier` will be generated by GitHub when we'll try to connect and provision resources on Azure. Here we created a Federated Credential for when we're executing a Github Action in `production environment`. - -Et voilà :sparkles: - -> __TLDR__ : For simplicity here we basically create an **identity that is able to create anything in your whole Azure subscription**. This has been made for simplicity but keep in mind that it doesnt follow the ***least privilege principle***. In real-world use-case, you better would have create an identity in the same resource group you wanna deploy (to reduce impact zone). - -Let's come back to our CD pipeline now. :eyes: - -## Deploy Infrastructure - -Before running our workflow we have two last things to add. - -If you understood well when we'll connect to Azure through `azure/login@v2` action and a federated credential will be fetched. - -This one should be used when provisionning resources (whether using bicep, az cli, terraform and so on). - -Also our federated credential is only valid on a GitHub job executed in production environment : that's why we set `environment: production`. - -That being said, add the following code in your workflow : - -```yaml -# on -# ... - -permissions: - # Require write permission to Fetch an OIDC token (required for federated credential) and write it - # It will be automatically used on actions / cli that needs it - id-token: write - -# env -# ... - -deploy_infrastructure: - runs-on: ubuntu-latest - environment: production # bind the job to the production environment -``` - -Now push your work into your remoted branch (whether a feature one or main branch depending how you decided to do) and go to GitHub Actions tab on your GitHub repository, a workflow should be executing. - -If you did well, everything should be green at the end :heavy_check_mark:. - -You can now come back to your Azure portal and look for **"App Service"** in the main search bar. - -You should find yours and on the Overview page see the `Default domain` property being displayed. - -Click on it (this could take time) and .. tadah ! You're App Service is created and online :star2:. - -![Default App Service page](./assets/app_service_default_page.png) - -Now let's deploy our backend API on it. :rocket: - -## Deploy Backend API - -Quite simple, first of all let's create the job responsible of deploying the backend API : `deploy_backend`. - -Add the following code in your GitHub workflow file : - -```yaml -deploy_backend: - runs-on: ubuntu-latest - needs: deploy_infrastructure - environment: production - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup .NET SDK 9.0.x - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '9.0.x' - - - name: Publish the app - run: dotnet publish -c Release --property:PublishDir=publish # Publish the app to the API project publish folder - working-directory: ./backend # Specify where to find the solution file in repository - - - name: Login to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Deploy backend to App Service - uses: azure/webapps-deploy@v2 - with: - app-name: ${{ needs.deploy_infrastructure.outputs.appServiceName }} # Access to the previous job output to get the appServiceName deployed with bicep - package: ./backend/ParkNDeploy.Api/publish # Path to the previously published app -``` - -Let's explain it a bit : -- `needs` clause allows you to wait a specified job before being executed, that way we ensure our infrastructure is provisionned before deploying our source code -- `Publish the app` is specific on your backend stack : for dotnet API, the dotnet publish is sufficient to create a deployable bundle -- `azure/webapps-deploy@v2` is the action that allows us to deploy our backend to Azure App Service - -:bulb: Notice the `app-name` property on the `azure/webapps-deploy@v2` action, we're specifying **needs.deploy_infrastructure.outputs.appServiceName** variable value. - -As its name implies, it is a value exposed by our `deploy_infrastructure` job. This trick allows us to pass value between jobs, but we need to expose those value. :smirk: - -Add the following code on the **deploy_infrastructure** job : - -```yaml -deploy_infrastructure: - runs-on: ubuntu-latest - environment: production - - outputs: - appServiceName: ${{ steps.bicep_deploy.outputs.appServiceName }} - - steps: - # ... -``` - -Through the `outputs` keyword we're exposing key-value pairs. - -Notice that here again we're using a value coming from a step, the `bicep_deploy` one : **steps.bicep_deploy.outputs.appServiceName** (that's the purpose of adding an **id** on this step :wink:). - -Well, our CD pipeline is finally ready so push the code and wait for our app to be deployed. :rocket: - -:bulb: In your GitHub workflow logs, you could access to the deployed API URL or you can also go through **Azure Portal** to your App Service instance and go click on **Default Domain** link. - -The app can take some time to launch, but when it's done .... WUT, we got a 404 Not Found ?! :cold_sweat: - -This is quite normal : it is a pure API that not exposing any endpoint on the root path, nevertheless ... - -For those who read the [backend readme file](../backend/README.md), you know that our API is exposing a Swagger UI right ? :smirk: - -Add `/swagger` to the url and you should see something like this : - -![backend swagger](./assets/backend_swagger.png) - -Congrats, our API is finally deployed ! :sparkles: - -Let's [add our UI](./step2_deploy_frontend.md) to visualize our parkings. :eyes: diff --git a/doc/step2_deploy_frontend.md b/doc/step2_deploy_frontend.md deleted file mode 100644 index 30299ec3..00000000 --- a/doc/step2_deploy_frontend.md +++ /dev/null @@ -1,236 +0,0 @@ -# Step 2 : Deploy ParkNDeploy Frontend - -## State about Azure resources - -In order to deploy a react application there's multiple ways of doing it : -- Azure App Service : quite a bit expensive (CPU wise) for an SPA -- [Azure Storage Account](https://byalexblog.net/article/react-azure-storage/) : basic storage resources allowing you to store and expose your html files (that's basically what a SPA is under the hood) -- Azure Static Web app : quite new resource created specially for static apps such as SPAs - -We will then go for this option : `Azure Static Web App`. - -This resource is offering tons of usefull [functionnalities](https://learn.microsoft.com/azure/static-web-apps/overview) but here we gonna stay simple : we just want to deploy our App on it. - -:bulb: For instance, we not gonna use [Preview environments](https://learn.microsoft.com/azure/static-web-apps/preview-environments) (but you can after this workshop if you are interested in :smirk:). - -Our new resource will be part of our previously created resource group, therefore our Azure infrastructure will look like this : - -``` -tenant <- no need to be provisionned (but authentication required ...) - └── subscription <- no need to be provisionned (but authentication required ...) - └── resourceGroup <- already provisionned - ├── appServicePlan <- already provisionned - | └── appService <- already provisionned - └── staticWebApp - └── ** Your Wonderfull Frontend ** -``` - -## Create Frontend Infrastructure - -As we did for our `App Service` resource, we gonna create a `Static Web App` bicep module. - -Create a `./infrastructure/modules/staticWebApp.bicep` file with the following code : - -```bicep -param location string -param project string -param identifier string - -resource swa 'Microsoft.Web/staticSites@2024-04-01 ' = { - name: '${project}-swa-${identifier}' - location: location - - sku: { - name: 'Free' - } - - properties: {} // Even empty, it's mandatory ... -} - -output swaName string = swa.name // Expose Static Web App name as we did for App Service for deployment purpose -``` - -Nothing fancy, it's quite a basic resource provisionning for our use case. -> **TLDR** : Do not miss the `properties` as the documentation doesnt make it mandatory, errors logs aren't telling explicitly to you, but it took me quite a time of debugging to figure out it is mandatory. :dizzy_face: -> -> Thanks this [post](https://github.com/Azure/static-web-apps/issues/868) for giving me the answer. :pray: - -Now call your module in your `./infrastructure/main.bicep` file : - -```bicep -param swaLocation string // Static Web App locations are limited, we need to add another variable - -// App Service & App Service Plan creation -// ... - -// Create the Static Web App through the StaticWebApp module -module staticWebApp 'modules/staticWebApp.bicep' = { - name: 'staticWebApp' - params: { - location: swaLocation - project: project - identifier: identifier - } -} - -output appServiceName string = appService.outputs.appServiceName // Export AppServiceName in order to deploy the API later on -output staticWebAppName string = staticWebApp.outputs.swaName // Export StaticWebAppName in order to deploy the Frontend late -``` - -You may have seen that we also expose the `staticWebAppName` to be able to deploy our code on it after. :wink: - -## Deploy Infrastructure - -Our CD pipeline for our infrastructure is actually almost ready to go. - -We first need to add our new bicep parameter : **swaLocation**. - -As we did with **location**, we gonna create a `AZURE_SWA_REGION` GitHub repository secret. - -Its value could be one of those : centralus, eastasia, eastus, eastus2, westeurope. - -:bulb: Take a consistent location with your resource group. :eyes: - -Then you just have to adjust our `bicep_deploy` step to use it : - -```yaml -- name: Deploy bicep - id: bicep_deploy - uses: azure/arm-deploy@v2 - with: - subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }} - region: ${{ secrets.AZURE_REGION }} - template: ./infrastructure/main.bicep - parameters: project=${{ vars.PROJECT_NAME }} location=${{ secrets.AZURE_REGION }} swaLocation=${{ secrets.AZURE_SWA_REGION }} identifier=${{ vars.AZURE_RESOURCE_IDENTIFIER }} - resourceGroupName: ${{ env.AZURE_RG_NAME }} -``` - -Perfect ! Now just push your code and trigger the GitHub workflow. :grin: - -When it's finished come back to the **Azure Portal** and look for your freshly deployed **"Static Web App"**. - -On the **Overview** page, look for the `URL` link and click on it. - -:bulb: The URL is pseudo-randomly generated, to have a custom domain it requires [few more steps](https://learn.microsoft.com/en-us/azure/static-web-apps/custom-domain). - -You should face with the following page telling you that you're resource is ready to host your code : - -![swa default page](./assets/swa_default_page.png) - -## Deploy Frontend App - -You may know what we gonna do now right ? :eyes: - -We'll create another deployment job for frontend, `deploy_frontend` for instance. - -Expose the `staticWebAppName` for our `deploy_infrastructure` job so that we could pass it to the dedicated action for Azure Static Web App resource. - -Commit, push and go ! - -So let's do it, expose the `staticWebAppName` output : - -```yaml -deploy_infrastructure: - runs-on: ubuntu-latest - environment: production - - outputs: - appServiceName: ${{ steps.bicep_deploy.outputs.appServiceName }} - staticWebAppName: ${{ steps.bicep_deploy.outputs.staticWebAppName }} - - steps: - # ... -``` - -Create the `deploy_frontend` job : - -```yaml -# deploy_infrastructure ... - -deploy_frontend: - runs-on: ubuntu-latest - needs: deploy_infrastructure - environment: production - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Build the app - run: npm install && npm run build - working-directory: ./frontend - - - name: Login to Azure - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Deploy frontend to Static Web App - uses: azure/static-web-apps-deploy@v1 - with: - app_location: frontend/dist - action: upload - skip_app_build: true - skip_api_build: true -``` - -> **Note** : You may have seen that now, two jobs will depends on **deploy_infrastructure** job. This will cause both **deploy_frontend** and **deploy_backend** to be run in parallel. :eyes: - -Commit, Push, run your workflow and ... what happened ? - -`azure/static-web-apps-deploy@v1` action is telling you that a deployment token is needed but ***what is that damn token*** ? :confused: - -→ Go on the [GitHub documentation](https://github.com/Azure/static-web-apps-deploy?tab=readme-ov-file) for this action and look for the file `action.yml` (it's basically the GitHub action entrypoint). - -You see that yes, an `azure_static_web_apps_api_token` is required. - -If you search a bit to find what it is you'll understand that when you deploy an **Azure Static Web App**, a specific deployment token is created. - -You then need to pass it to the `azure/static-web-apps-deploy@v1` action to be able to deploy your app. - -Here you have two options : -- Get it manually from your resource on **Azure Portal**, store it as a secret, and use it in your workflow -- Get it automatically during your job through `az cli`, assign it in a workflow scoped variable and use it directly - -To be consistent with the workshop, we'll go on the automated way (also to avoid push ing your secret on the web ...) but you could try the manual way if you want. :wink: - -Add the following step in your `deploy_frontend` job : - -```yaml - -# Login to Azure -# ... - -- name: Get Static Web App deployment token - run: | - SWA_DEPLOYMENT_TOKEN=$(az staticwebapp secrets list -n ${{ needs.deploy_infrastructure.outputs.staticWebAppName }} -o tsv --query properties.apiKey) - echo SWA_DEPLOYMENT_TOKEN=$SWA_DEPLOYMENT_TOKEN >> $GITHUB_ENV - -# Deploy frontend to Static Web App -# ... -``` - -and pass your freshly new `SWA_DEPLOYMENT_TOKEN` variable to your `azure/static-web-apps-deploy@v1` action : - -```yaml -- name: Deploy frontend to Static Web App - uses: azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ env.SWA_DEPLOYMENT_TOKEN }} - app_location: frontend/dist - action: upload - skip_app_build: true - skip_api_build: true -``` - -Now you can push your code again, trigger your workflow again and tadah. :sparkles: - -Our app is now deployed let see how wonderfull it looks ! - -Wait ... seems there's a problem with the backend no ? :disappointed_relieved: - -![react app no backend error](./assets/react_app_no_backend_error.png) - -Let's troobleshoot this [connexion problem](step3_troobleshooting_connexion.md). :eyeglasses: diff --git a/doc/step3_troobleshooting_connexion.md b/doc/step3_troobleshooting_connexion.md deleted file mode 100644 index bc85a790..00000000 --- a/doc/step3_troobleshooting_connexion.md +++ /dev/null @@ -1,109 +0,0 @@ -# Step 3 : Troobleshooting Frontend <==> API connexion - -## Find out the problem - -Let's open your navigator debugger, go on the **Network tab** and reload so that we could see the request made on the API. - -You see ? The Frontend is calling the Backend but using the Frontend URL, weird huh ? - -It seems our call is being redirected by a proxy or something similar. :eyes: - -Let's take a step back. - -What would happend if we deployed our app in a basic provider like OVH for instance ? - -You wouldn't have such a proxy (by default), and would have seen the API URL in the chrome debugger Network tab. - -You would then have to fix CORS error by adding a policy on your backend (whether on the App Service or inside the Backend App directly), et voilà ! - -But that's not how Azure Static App works. :grimacing: - -We'll have to use a what we call a `Linked Backend`. - -Of course Static Web App could be linked to multiple backend with differents [types](https://learn.microsoft.com/azure/static-web-apps/apis-overview#api-options) (depending on sku used). - -In order to link our Azure App Service instance, we'll need the **Standard** sku (which is not free :smirk:). - -> **Notes** : Some [constraints](https://learn.microsoft.com/azure/static-web-apps/apis-overview#constraints) have to be keep in mind : -> - need to have a `/api` basePath on your API, that's why it's mentionned in the [backend readme file](../backend/README.md). -> - backend will, by default, no longer be accessible elsewhere than our Static Web App - -## Frontend Infrastructure update - -Let's start by updating the Static Web App sku : - -```bicep -sku: { - name: 'Standard' - tier: 'Standard' -} -``` - -Then create a new bicep module `./infrastructure/modules/staticWebAppBackend.bicep` : - -```bicep -param backendBindedResourceId string -param swaName string -param location string - -resource staticWebAppBackend 'Microsoft.Web/staticSites/linkedBackends@2022-03-01' = { - name: '${swaName}/backend' - properties: { - backendResourceId: backendBindedResourceId - region: location - } -} -``` - -Notice that we need to pass our backend resource id in order to create our link. :eyes: - -So well, let's update our `./infrastructure/modules/appService.bicep` file to expose it : - -```bicep -// Previous outputs -// ... -output appServiceId string = app.id -``` - -And finally let's update our main bicep to create our new resource and pass the backend resource id :grin: : - -```bicep -// Previous resources -// ... -module staticWebAppBackend 'modules/staticWebAppBackend.bicep' = { - name: 'staticWebAppBackend' - params: { - backendBindedResourceId: appService.outputs.appServiceId - swaName: staticWebApp.outputs.swaName - location: location - } -} - -// Outputs -// ... -``` - -Perfect our bicep project now meets our goal. :heavy_check_mark: - -Push your code, trigger the workflow for the last time, and see the final result. :wink: - -## Final test - -If you followed every single steps, you should end with something like this : - -![final app](./assets/final_deployed_app.png) - -Congratulation you just ended this workshop ! :sparkles: - -You'll now have three options : -- Continue with [exercises](step4_further_improvements.md) to make some improvements on our project :eyes: -- Studying by your own following some [additionnal resources](to_go_further.md) :rocket: -- Leave it there, you heard enough about DevOps for now :dizzy_face: - -Whatever, I hope you enjoyed this initiation DevOps workshop as much as me to produce it. :metal: - -Feel free to leave me a comment on my [email adress](mailto:ju.raillard@hotmail.fr) or on my [LinkedIn](https://www.linkedin.com/in/julien-raillard/). :blush: - -You can also leave a star on the main project you forked to make it more visible. :pray: - -Thank you for following along, and happy coding! :computer: diff --git a/doc/step4_further_improvements.md b/doc/step4_further_improvements.md deleted file mode 100644 index d2eec117..00000000 --- a/doc/step4_further_improvements.md +++ /dev/null @@ -1,85 +0,0 @@ -# Step 4 : Further improvements - -Below you'll find some suggestions to improve our project. - -Those will be splited in two types : functionnalities, refactorings. - -As its name implies, it would be better to implement functionnalities before thinking about refactorings but the choice is yours. :wink: - -Stars number will indicate the difficulty. - -Every suggested improvements will be followed with clue(s) to achieve it. - -## New functionnalities - -:star: Trigger the deployment pipeline using git tag - -→ In Git, a tag is a way to mark our source code and associate a version on it. - -:bulb: You can restrict the tag to follow the semantic versionning : `MAJOR.MINOR.PATCH` , where MAJOR, MINOR and PATCH are a digit from 0 to 9. - -:star::star: Create a CI Pipeline specific for Merge Requests - -→ The pipeline should **only** be triggered when a merge request is created - -→ The pipeline would (in parallel or sequentially) : -- For Frontend app : - - build the react app - - run the linting script - - :bulb: See package.json file ... :eyes: - -- For Backend app : - - [build the dotnet app in release mode](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-build) - -→ This aim to have a minimal check (the solution compiles) when someone is making a merge request on your repository before reviewing the code and merge it :wink: - -:star::star: Create the CI Pipeline for releases and connect it to our CD Pipeline - -→ At this step, CI release pipeline could just be single or multiple jobs (for both frontend & backend) executed before CD pipeline Jobs. Spliting into two distinct workflows is further refactoring step. - -→ The CI release pipeline would (in parallel or sequentially) : -- For Frontend app : - - build the react app - - create and push the artifact -- For Backend app : - - publish the dotnet app - - create and push the artifact - -→ The CD pipeline should be automatically triggered when your CI pipeline is ended - -→ The CD pipeline should download the previous uploaded artifacts instead of building it - -:star::star::star: Display the app version in the Frontend app - -→ You need to be able to trigger your pipeline by a tag, in order to get the version - -→ You not necessarily need to have a CI release pipeline implemented but you can - -→ You need to pass, somehow, the version to the App in order to display it, the [frontend README file](../frontend/README.md) might give your somes clues ... :eyes: - -:star::star::star::star: Your package version displayed should be consistent in your repository. - -→ Means we can see the version in source code somehow - -## Refactorings - -:star: Match the least privilege principle - -→ Refactor the permissions to be retrieve only when needed (not on workflow level here ... :eyes:) - -:star::star: Split your CI and CD pipeline into two workflows - -→ A tag should trigger CI workflow and this workflow should trigger CD workflow if it pass - -:bulb: Test the two possibilities and analyze pros and cons : -- workflow_run -- workflow_call - -When you're done, compare your finds with this [article](https://jiminbyun.medium.com/github-actions-workflow-run-vs-workflow-call-3f1a5c6e19d4). :eyes: - -:star::star::star: Allow developer team to trigger the CD pipeline manually by specifying artifacts - -→ You'll need to allow manual workflow trigger and retrieve the artifacts url using the GitHub API - -→ Remember that workflow_dispatch will only works if youre workflows are on default branch or if you're using GitHub CLI to trigger it. diff --git a/doc/to_go_further.md b/doc/to_go_further.md deleted file mode 100644 index cc09372c..00000000 --- a/doc/to_go_further.md +++ /dev/null @@ -1,13 +0,0 @@ -# Further Learning Resources for DevOps Enthusiasts - -Did you fall in love with DevOps in this course or just want go deeper ? :eyes: - -The following ressources might fit your needs as they cover all the DevOps concepts :grin: - -:arrow_forward: [John Savill's Masterclass](https://www.youtube.com/watch?v=R74bm8IGu2M&list=PLlVtbbG169nFr8RzQ4GIxUEznpNR53ERq) - -→ A comprehensive YouTube playlist created by John Savill, an australian Microsoft MVP. This masterclass covers a wide range of topics related to DevOps, cloud computing, and Microsoft technologies. John Savill has also created numerous playlists for almost every Microsoft certification, making his content a valuable resource for both beginners and advanced learners. - -:book: [Designing and Implementing Microsoft DevOps Solutions course](https://learn.microsoft.com/credentials/certifications/exams/az-400/) - -→ This course, available on Microsoft Learn, is designed to help you prepare for the AZ-400 certification exam. It includes a mix of hands-on labs and written materials that cover various DevOps practices and tools. By completing this course, you will gain the knowledge and skills needed to become a certified DevOps Engineer Expert. \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index a0946957..9cbaa13d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "private": true, - "version": "0.0.0", + "version": "8.0.4", "type": "module", "scripts": { "dev": "vite --port 5175", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 986ff9bb..fa26e17b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { LoadingSpinner } from "@/components/ui/loadingspinner"; import ParkingListFilters from "@/components/ParkingList/ParkingListFilters"; import { useParkingSearchStore } from "@/stores/parkingSearchStore"; +const APP_VERSION = import.meta.env.VITE_APP_VERSION || 'local-dev'; function App() { const { parkingName } = useParkingSearchStore(); @@ -31,7 +32,12 @@ function App() { {isPending && } {isError && Something went wrong with the backend ...} {data && } + +
+ release: {APP_VERSION} +
+ ); } diff --git a/infrastructure/main.bicep b/infrastructure/main.bicep new file mode 100644 index 00000000..1b58b1b6 --- /dev/null +++ b/infrastructure/main.bicep @@ -0,0 +1,59 @@ +targetScope = 'resourceGroup' // We'll deploy the resources in the provided resource group + +// Parameters to easily construct resource names +param location string +param project string + +// Here we'll add an identifier to create a unique name for the App Service Plan, for example your trigram, so that everyone could deploy his own parkndeploy instance +param identifier string + +// Static web app location +param swaLocation string + +// Create the AppServicePlan through the AppServicePlan module +module appServicePlan 'modules/appServicePlan.bicep' = { + name: 'appServicePlan' + params: { + location: location + project: project + identifier: identifier + } +} + +// Create the AppService through the AppService module +module appService 'modules/appService.bicep' = { + name: 'appService' + params: { + location: location + project: project + identifier: identifier + planId: appServicePlan.outputs.planId // Use the appServicePlan output to get its id back => an App Service needs to reference its App Service Plan + } +} + +// Create the Static Web App through the StaticWebApp module +module staticWebApp 'modules/staticWebApp.bicep' = { + name: 'staticWebApp' + params: { + location: swaLocation + project: project + identifier: identifier + } +} + +// Previous resources +// ... +module staticWebAppBackend 'modules/staticWebAppBackend.bicep' = { + name: 'staticWebAppBackend' + params: { + backendBindedResourceId: appService.outputs.appServiceId + swaName: staticWebApp.outputs.swaName + location: location + } +} + +// Outputs +// ... +// Export App Service Name +output appServiceName string = appService.outputs.appServiceName +output staticWebAppName string = staticWebApp.outputs.swaName // Export StaticWebAppName in order to deploy the Frontend late diff --git a/infrastructure/modules/appService.bicep b/infrastructure/modules/appService.bicep new file mode 100644 index 00000000..dbe7a75b --- /dev/null +++ b/infrastructure/modules/appService.bicep @@ -0,0 +1,23 @@ +param location string +param project string +param identifier string + +// App Service Plan identifier that will host our App Service +param planId string + +resource app 'Microsoft.Web/sites@2022-03-01' = { + name: '${project}-app-${identifier}' + location: location + + properties: { + serverFarmId: planId + reserved: true + + siteConfig: { + linuxFxVersion: 'DOTNETCORE|9.0' // Specify to setup the .NET Core 9.0 runtime (used by our backend API) on the Linux machine under the hood + } + } +} + +output appServiceName string = app.name // Export the App Service name for deployment +output appServiceId string = app.id diff --git a/infrastructure/modules/appServicePlan.bicep b/infrastructure/modules/appServicePlan.bicep new file mode 100644 index 00000000..c871995f --- /dev/null +++ b/infrastructure/modules/appServicePlan.bicep @@ -0,0 +1,20 @@ +param location string +param project string +param identifier string + +resource plan 'Microsoft.Web/serverfarms@2022-09-01' = { + name: '${project}-plan-${identifier}' + location: location + + sku: { + name: 'F1' // We use F1 pricing plan (free one) as we don't need specific features + } + + kind: 'app,linux' // Allow to deploy on an App Service using Linux OS + + properties: { + reserved: true // Specifity of App Service with Linux OS + } +} + +output planId string = plan.id // Export the App Service identifier \ No newline at end of file diff --git a/infrastructure/modules/staticWebApp.bicep b/infrastructure/modules/staticWebApp.bicep new file mode 100644 index 00000000..949c3043 --- /dev/null +++ b/infrastructure/modules/staticWebApp.bicep @@ -0,0 +1,17 @@ +param location string +param project string +param identifier string + +resource swa 'Microsoft.Web/staticSites@2024-04-01' = { + name: '${project}-swa-${identifier}' + location: location + + sku: { + name: 'Standard' + tier: 'Standard' +} + + properties: {} // Even empty, it's mandatory ... +} + +output swaName string = swa.name // Expose Static Web App name as we did for App Service for deployment purpose diff --git a/infrastructure/modules/staticWebAppBackend.bicep b/infrastructure/modules/staticWebAppBackend.bicep new file mode 100644 index 00000000..4e902770 --- /dev/null +++ b/infrastructure/modules/staticWebAppBackend.bicep @@ -0,0 +1,11 @@ +param backendBindedResourceId string +param swaName string +param location string + +resource staticWebAppBackend 'Microsoft.Web/staticSites/linkedBackends@2022-03-01' = { + name: '${swaName}/backend' + properties: { + backendResourceId: backendBindedResourceId + region: location + } +}