From d87f756b297d331e94564a56a7060f0e739c604d Mon Sep 17 00:00:00 2001 From: Diogo Barros Date: Mon, 9 Dec 2024 13:21:31 +0000 Subject: [PATCH 01/21] Simplicity Removals --- .gitattributes | 12 -- .github/ISSUE_TEMPLATE | 40 ----- .github/dependabot.yml | 7 - .../workflows/call-docker-build-result.yaml | 82 --------- .github/workflows/call-docker-build-vote.yaml | 82 --------- .../workflows/call-docker-build-worker.yaml | 82 --------- .vscode/launch.json | 20 --- MAINTAINERS | 9 - README.md | 170 +++++++++++++----- architecture.excalidraw.png | Bin 151461 -> 0 bytes docker-compose.images.yml | 16 +- docker-compose.yml | 4 - docker-stack.yml | 53 ------ healthchecks/postgres.sh | 21 --- healthchecks/redis.sh | 10 -- k8s-specifications/db-deployment.yaml | 33 ---- k8s-specifications/db-service.yaml | 15 -- k8s-specifications/redis-deployment.yaml | 28 --- k8s-specifications/redis-service.yaml | 15 -- k8s-specifications/result-deployment.yaml | 22 --- k8s-specifications/result-service.yaml | 15 -- k8s-specifications/vote-deployment.yaml | 22 --- k8s-specifications/vote-service.yaml | 16 -- k8s-specifications/worker-deployment.yaml | 19 -- result/docker-compose.test.yml | 62 ------- result/tests/Dockerfile | 12 -- result/tests/render.js | 15 -- result/tests/tests.sh | 20 --- seed-data/Dockerfile | 16 -- seed-data/generate-votes.sh | 6 - seed-data/make-data.py | 13 -- 31 files changed, 132 insertions(+), 805 deletions(-) delete mode 100644 .gitattributes delete mode 100644 .github/ISSUE_TEMPLATE delete mode 100644 .github/dependabot.yml delete mode 100644 .github/workflows/call-docker-build-result.yaml delete mode 100644 .github/workflows/call-docker-build-vote.yaml delete mode 100644 .github/workflows/call-docker-build-worker.yaml delete mode 100644 .vscode/launch.json delete mode 100644 MAINTAINERS delete mode 100644 architecture.excalidraw.png delete mode 100644 docker-stack.yml delete mode 100755 healthchecks/postgres.sh delete mode 100755 healthchecks/redis.sh delete mode 100644 k8s-specifications/db-deployment.yaml delete mode 100644 k8s-specifications/db-service.yaml delete mode 100644 k8s-specifications/redis-deployment.yaml delete mode 100644 k8s-specifications/redis-service.yaml delete mode 100644 k8s-specifications/result-deployment.yaml delete mode 100644 k8s-specifications/result-service.yaml delete mode 100644 k8s-specifications/vote-deployment.yaml delete mode 100644 k8s-specifications/vote-service.yaml delete mode 100644 k8s-specifications/worker-deployment.yaml delete mode 100644 result/docker-compose.test.yml delete mode 100644 result/tests/Dockerfile delete mode 100644 result/tests/render.js delete mode 100755 result/tests/tests.sh delete mode 100644 seed-data/Dockerfile delete mode 100755 seed-data/generate-votes.sh delete mode 100644 seed-data/make-data.py diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index abe6ae79f3..0000000000 --- a/.gitattributes +++ /dev/null @@ -1,12 +0,0 @@ -# Set default behavior to automatically normalize line endings. -* text=auto - -# Force batch scripts to always use CRLF line endings so that if a repo is accessed -# in Windows via a file share from Linux, the scripts will work. -*.{cmd,[cC][mM][dD]} text eol=crlf -*.{bat,[bB][aA][tT]} text eol=crlf -*.{ics,[iI][cC][sS]} text eol=crlf - -# Force bash scripts to always use LF line endings so that if a repo is accessed -# in Unix via a file share from Windows, the scripts will work. -*.sh text eol=lf diff --git a/.github/ISSUE_TEMPLATE b/.github/ISSUE_TEMPLATE deleted file mode 100644 index 2ad58e518a..0000000000 --- a/.github/ISSUE_TEMPLATE +++ /dev/null @@ -1,40 +0,0 @@ -** PLEASE ONLY USE THIS ISSUE TRACKER TO SUBMIT ISSUES WITH THE EXAMPLE VOTING APP ** - -* If you have a bug working with Docker itself, not related to these labs, please file the bug on the [Docker repo](https://github.com/docker/docker) * -* If you would like general support figuring out how to do something with Docker, please use the Docker Slack channel. If you're not on that channel, sign up for the [Docker Community](http://dockr.ly/MeetUp) and you'll get an invite. * -* Or go to the [Docker Forums](https://forums.docker.com/) * - -Please provide the following information so we can assess the issue you're having - -**Description** - - - -**Steps to reproduce the issue, if relevant:** -1. -2. -3. - -**Describe the results you received:** - - -**Describe the results you expected:** - - -**Additional information you deem important (e.g. issue happens only occasionally):** - -**Output of `docker version`:** - -``` -(paste your output here) -``` - -**Output of `docker info`:** - -``` -(paste your output here) -``` - -**Additional environment details (AWS, Docker for Mac, Docker for Windows, VirtualBox, physical, etc.):** diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index f9ecf576e1..0000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,7 +0,0 @@ -version: 2 -updates: - # Maintain dependencies for GitHub Actions - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" diff --git a/.github/workflows/call-docker-build-result.yaml b/.github/workflows/call-docker-build-result.yaml deleted file mode 100644 index a946a87b03..0000000000 --- a/.github/workflows/call-docker-build-result.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build Result -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml - -on: - # we want pull requests so we can build(test) but not push to image registry - push: - branches: - - 'main' - # only build when important files change - paths: - - 'result/**' - - '.github/workflows/call-docker-build-result.yaml' - pull_request: - branches: - - 'main' - # only build when important files change - paths: - - 'result/**' - - '.github/workflows/call-docker-build-result.yaml' - -jobs: - call-docker-build: - - name: Result Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-result - dockersamples/examplevotingapp_result - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: result - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false diff --git a/.github/workflows/call-docker-build-vote.yaml b/.github/workflows/call-docker-build-vote.yaml deleted file mode 100644 index cb4a484a2a..0000000000 --- a/.github/workflows/call-docker-build-vote.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build Vote -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml - -on: - # we want pull requests so we can build(test) but not push to image registry - push: - branches: - - 'main' - # only build when important files change - paths: - - 'vote/**' - - '.github/workflows/call-docker-build-vote.yaml' - pull_request: - branches: - - 'main' - # only build when important files change - paths: - - 'vote/**' - - '.github/workflows/call-docker-build-vote.yaml' - -jobs: - call-docker-build: - - name: Vote Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-vote - dockersamples/examplevotingapp_vote - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=before,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=raw,value=after,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: vote - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false diff --git a/.github/workflows/call-docker-build-worker.yaml b/.github/workflows/call-docker-build-worker.yaml deleted file mode 100644 index 5abfb6bc9c..0000000000 --- a/.github/workflows/call-docker-build-worker.yaml +++ /dev/null @@ -1,82 +0,0 @@ -name: Build Worker -# template source: https://github.com/dockersamples/.github/blob/main/templates/call-docker-build.yaml - -on: - # we want pull requests so we can build(test) but not push to image registry - push: - branches: - - 'main' - # only build when important files change - paths: - - 'worker/**' - - '.github/workflows/call-docker-build-worker.yaml' - pull_request: - branches: - - 'main' - # only build when important files change - paths: - - 'worker/**' - - '.github/workflows/call-docker-build-worker.yaml' - -jobs: - call-docker-build: - - name: Worker Call Docker Build - - uses: dockersamples/.github/.github/workflows/reusable-docker-build.yaml@main - - permissions: - contents: read - packages: write # needed to push docker image to ghcr.io - pull-requests: write # needed to create and update comments in PRs - - secrets: - - # Only needed if with:dockerhub-enable is true below - dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} - - # Only needed if with:dockerhub-enable is true below - dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - - with: - - ### REQUIRED - ### ENABLE ONE OR BOTH REGISTRIES - ### tell docker where to push. - ### NOTE if Docker Hub is set to true, you must set secrets above and also add account/repo/tags below - dockerhub-enable: true - ghcr-enable: true - - ### REQUIRED - ### A list of the account/repo names for docker build. List should match what's enabled above - ### defaults to: - image-names: | - ghcr.io/dockersamples/example-voting-app-worker - dockersamples/examplevotingapp_worker - - ### REQUIRED set rules for tagging images, based on special action syntax: - ### https://github.com/docker/metadata-action#tags-input - ### defaults to: - tag-rules: | - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=pr - - ### path to where docker should copy files into image - ### defaults to root of repository (.) - context: worker - - ### Dockerfile alternate name. Default is Dockerfile (relative to context path) - # file: Containerfile - - ### build stage to target, defaults to empty, which builds to last stage in Dockerfile - # target: - - ### platforms to build for, defaults to linux/amd64 - ### other options: linux/amd64,linux/arm64,linux/arm/v7 - # FIXME worker arm/v7 support doesn't build in .net core 3.1 with QEMU - # a fix would likely run the .net build on amd64 but with a target of arm/v7 - platforms: linux/amd64,linux/arm64,linux/arm/v7 - - ### Create a PR comment with image tags and labels - ### defaults to false - # comment-enable: false diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 3e9f0bd7ac..0000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Node: Results debugger", - "type": "node", - "request": "attach", - "port": 9229, - "address": "localhost", - "skipFiles": [ - "/**" - ], - "remoteRoot": "/app", - "localRoot": "${workspaceFolder}/result" - } - ] -} \ No newline at end of file diff --git a/MAINTAINERS b/MAINTAINERS deleted file mode 100644 index a7f9d7c983..0000000000 --- a/MAINTAINERS +++ /dev/null @@ -1,9 +0,0 @@ -Bret Fisher -Michael Irwin - -# Alumni, thanks for your work! -Aanand Prasad -Ben Firshman -Fernando Mayo -Mano Marks -Maxime Heckel diff --git a/README.md b/README.md index 8516424ba1..2374b71a02 100644 --- a/README.md +++ b/README.md @@ -1,65 +1,155 @@ -# Example Voting App + -A simple distributed application running across multiple Docker containers. +--- -## Getting started +# Multi-Stack Voting Application -Download [Docker Desktop](https://www.docker.com/products/docker-desktop) for Mac or Windows. [Docker Compose](https://docs.docker.com/compose) will be automatically installed. On Linux, make sure you have the latest version of [Compose](https://docs.docker.com/compose/install/). +**Welcome to your DevOps practice project!** This repository hosts a multi-stack voting application composed of several services, each implemented in a different language and technology stack. The goal is to help you gain experience with containerization, orchestration, and running a distributed set of services—both individually and as part of a unified system. -This solution uses Python, Node.js, .NET, with Redis for messaging and Postgres for storage. +This application, while simple, uses multiple components commonly found in modern distributed architectures, giving you hands-on practice in connecting services, handling containers, and working with basic infrastructure automation. -Run in this directory to build and run the app: +## Application Overview -```shell -docker compose up -``` +The voting application includes: -The `vote` app will be running at [http://localhost:8080](http://localhost:8080), and the `results` will be at [http://localhost:8081](http://localhost:8081). +- **Vote (Python)**: A Python Flask-based web application where users can vote between two options. +- **Redis (in-memory queue)**: Collects incoming votes and temporarily stores them. +- **Worker (.NET)**: A .NET 7.0-based service that consumes votes from Redis and persists them into a database. +- **Postgres (Database)**: Stores votes for long-term persistence. +- **Result (Node.js)**: A Node.js/Express web application that displays the vote counts in real time. -Alternately, if you want to run it on a [Docker Swarm](https://docs.docker.com/engine/swarm/), first make sure you have a swarm. If you don't, run: +### Why This Setup? -```shell -docker swarm init -``` +The goal is to introduce you to a variety of languages, tools, and frameworks in one place. This is **not** a perfect production design. Instead, it’s intentionally diverse to help you: -Once you have your swarm, in this directory run: +- Work with multiple runtimes and languages (Python, Node.js, .NET). +- Interact with services like Redis and Postgres. +- Containerize applications using Docker. +- Use Docker Compose to orchestrate and manage multiple services together. -```shell -docker stack deploy --compose-file docker-stack.yml vote -``` +By dealing with this “messy” environment, you’ll build real-world problem-solving skills. After this project, you should feel more confident tackling more complex deployments and troubleshooting issues in containerized, multi-service setups. -## Run the app in Kubernetes +--- -The folder k8s-specifications contains the YAML specifications of the Voting App's services. +## How to Run Each Component -Run the following command to create the deployments and services. Note it will create these resources in your current namespace (`default` if you haven't changed it.) +### Running the Vote Service (Python) Locally (No Docker) -```shell -kubectl create -f k8s-specifications/ -``` +1. Ensure you have Python 3.10+ installed. +2. Navigate to the `vote` directory: + ```bash + cd vote + pip install -r requirements.txt + python app.py + ``` + Access the vote interface at [http://localhost:5000](http://localhost:5000). + +### Running Redis Locally (No Docker) + +1. Install Redis on your system ([https://redis.io/docs/getting-started/](https://redis.io/docs/getting-started/)). +2. Start Redis: + ```bash + redis-server + ``` + Redis will be available at `localhost:6379`. + +### Running the Worker (C#/.NET) Locally (No Docker) + +1. Ensure .NET 7.0 SDK is installed. +2. Navigate to `worker`: + ```bash + cd worker + dotnet restore + dotnet run + ``` + The worker will attempt to connect to Redis and Postgres when available. + +### Running Postgres Locally (No Docker) + +1. Install Postgres from [https://www.postgresql.org/download/](https://www.postgresql.org/download/). +2. Start Postgres, note the username and password (default `postgres`/`postgres`): + ```bash + # On many systems, Postgres runs as a service once installed. + ``` + Postgres will be available at `localhost:5432`. + +### Running the Result Service (Node.js) Locally (No Docker) -The `vote` web app is then available on port 31000 on each host of the cluster, the `result` web app is available on port 31001. +1. Ensure Node.js 18+ is installed. +2. Navigate to `result`: + ```bash + cd result + npm install + node server.js + ``` + Access the results interface at [http://localhost:4000](http://localhost:4000). -To remove them, run: +**Note:** To get the entire system working end-to-end (i.e., votes flowing through Redis, processed by the worker, stored in Postgres, and displayed by the result app), you’ll need to ensure each component is running and that connection strings or environment variables point to the correct services. -```shell -kubectl delete -f k8s-specifications/ +--- + +## Running the Entire Stack in Docker + +### Building and Running Individual Services + +You can build each service with Docker and run them individually: + +- **Vote (Python)**: + ```bash + docker build -t myorg/vote:latest ./vote + docker run --name vote -p 8080:80 myorg/vote:latest + ``` + Visit [http://localhost:8080](http://localhost:8080). + +- **Redis** (official image, no build needed): + ```bash + docker run --name redis -p 6379:6379 redis:alpine + ``` + +- **Worker (.NET)**: + ```bash + docker build -t myorg/worker:latest ./worker + docker run --name worker myorg/worker:latest + ``` + +- **Postgres**: + ```bash + docker run --name db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:15-alpine + ``` + +- **Result (Node.js)**: + ```bash + docker build -t myorg/result:latest ./result + docker run --name result -p 8081:80 myorg/result:latest + ``` + Visit [http://localhost:8081](http://localhost:8081). + +### Using Docker Compose + +The easiest way to run the entire stack is via Docker Compose. From the project root directory: + +```bash +docker compose up ``` -## Architecture +This will: + +- Build and run the vote, worker, and result services. +- Run Redis and Postgres from their official images. +- Set up networks, volumes, and environment variables so all services can communicate. -![Architecture diagram](architecture.excalidraw.png) +Visit [http://localhost:8080](http://localhost:8080) to vote and [http://localhost:8081](http://localhost:8081) to see results. -* A front-end web app in [Python](/vote) which lets you vote between two options -* A [Redis](https://hub.docker.com/_/redis/) which collects new votes -* A [.NET](/worker/) worker which consumes votes and stores them in… -* A [Postgres](https://hub.docker.com/_/postgres/) database backed by a Docker volume -* A [Node.js](/result) web app which shows the results of the voting in real time +--- -## Notes +## Notes on Platforms (arm64 vs amd64) + +If you’re on an arm64 machine (e.g., Apple Silicon M1/M2) and encounter issues with images or dependencies that assume amd64, you can use Docker `buildx`: + +```bash +docker buildx build --platform linux/amd64 -t myorg/worker:latest ./worker +``` -The voting application only accepts one vote per client browser. It does not register additional votes if a vote has already been submitted from a client. +This ensures the image is built for the desired platform. -This isn't an example of a properly architected perfectly designed distributed app... it's just a simple -example of the various types of pieces and languages you might see (queues, persistent data, etc), and how to -deal with them in Docker at a basic level. +--- diff --git a/architecture.excalidraw.png b/architecture.excalidraw.png deleted file mode 100644 index 643bacdbe9c2fed325b6d664d4b9b1442172f989..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 151461 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 c;LO*ScjP@+Q-8z? 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

05ORAt!a<(kkR+= z_;mb^_53BBhp`{jk|s;RWKa~SPZ46dx=eFHCvVh_(097c5z^na#|O2?8%_YTub%u% ziE-p8x!U-{LurrQ)za4MF(p}$Jv^BkdvA0OR@HRn;ZBmvnN^fk&yMX$T3XM{s~>{2 zI9Bjdg@nzUiJoHE#>6(g?6&dVOCrWc94()DdE4$et>moWT|CE2Rwti1f>Brknb0XT zu!@)3M7~Lqtj09t3>WT_v;U^~ilP-u`%}e= zZuazl>Nw?3r-#pWNl2?}Q3qCSFTb7Gy8*2|>fdV&n2_ATkDBy>7E}}VQ$hnA%$P$d z+dmN1)pZq8Y0f9`e<8eno-!eBw@K zx!ub0GM2&gb3rIF{9W8}&o;C;Bt)@)3^--w+{nWqU(i+s@=W!( z_G`n3v$cEu(rX$zeJ#I=u=yN~ThTz+ktxe#97H_sB=*ynIcYH#m{9knx<90pD;M{? z^1Uhd3s<3QE@C|jw?&?7cKNxVX4qJP(}(D{oD=_vJMF8VF4yrNf2{1g^)CUH9nBJi z*&Wxu)^|U`K(l87cTr1Wkvbp^xP)H>`}cM;8FIa_^+rX6TCsXcNaS0)&}t~Z=X6Wr z^r{v(UU|jBZ00pC9hjI$y|AF^=8AcrR?2r|GjKnKu)GnaaR24RFt+a!HfB&Kgg22B zP`hdGrv*hMO``TYr_zeuwlvWQ0;~0a0T1~5e%d4cAVn{@+mAxaE}8XEbh8zr8BNy& znZn#!Qm*I26;pH62LD|sTi(|Edb7V!`*{wQY6HdKczABSKmA+XwQpr>_holrWJu+9 zh5!KL1nTL>RB$)SUb4=nEd|H1&5B+*T^ai$2nImO21c?(t@_cY<}5wVm52L`B=NS z8(%B2<=F7@)!?eQvi=K`F+wZ~ibr&f1F)r!YViH&@t?l`#dkUDw49c;$V zxLyapc=X`Qz}E>N9-Q=+O--9$4Zl9guTdDkt-r#^DY_wq9B0YfTg@>`Bj7B7;H^;F z&(8!$o=)fst&(4o@H>ew=k`VS^GURw6VG4}3b}432U-}6s2?Wi2vU9PT*rcSKXgnS3BM z-=Hnz+a^we*^0yH{~4es{L|fqOnc)#Z~u+16`Q=GqIBB_J(kaT6@e_vJ^k`?9^N6t z*+d4H_MTA=gA~A!=({GP)7TTfa1e)@`7Y?vNgX8I>a_<>}V8fP7TNZ!}QLw98^X|LONi{br`ztnn<3jzx z40jbkfTUG1Mtc;;9~eJp*Gv4g9BM38*u4J_^6zlsdtXsjcpMD_%WGmD-O%lO_wVNR zA{@w)QO(RN8;fy7?g#?!yqZ*(oCZH$?$qkeR$`{FHx>N#BMmdjibKOe%OLNB?wan~#jbh^fm}jD6gIiVe zZY?@9OcA3$0V^>;7yb<~0J12~ zV?rIUFh7uydi^)txAt1{={<#>p5AZ~-7?7*TB{x4t!Gwid86*`Ugd;)p4Ob1@R zAmyuwrynH)Cy5n{K%CNRZ06wc@bEbgmzF0(hLJJ-A11_Xf$P?v8QJ2YWCIsw9~5T1 zYh`Q>$}zuuepXcl&?v(CkiBT{)HswGe%L@TI}Lr{(}11jhWAx7BJt%uejmj&4_*X0 z9PibaI@gTxjZKQG0Y!G1I5qe?gBR!TBbBxooAaCvEC^w5WnTd_jIh48=7+WHt{aal zH!dr6@F7Eh8dA(-8~ml}RbHLzVH{$N6lnN?KKp|MiS^@eMOu`@Cx(P8`-6c?I-kUE zz0>J~ih)tRF%DA0J`-55<%NO^SBV{9^wZE>IAk-q&%)ixdIqf6zwpH)TQj04e1$Y7 zVD1jc>V71Cu@dQ`6P!`#8M;s$0&Rmezgd?V`oufxANPjzmbycLpE# zppmi)QNh6~A3byuK(r_}o$w_!l13(L#y^W+%Wc%)IVQKmdXSeu6S75>txVtc-@nCXzT`(v|ot>x1NwL6>2WiR*@KQq^h$U zu97p*Q;r%N4=z!uesAdfHxdjm=m6pCC$R}mJQM`D%BwH*J*oO#yg+mzfMMr=f)}9* zFY|;%?m@@(bD0}&>B_dR82-rY1<4AHF}BQqjCLqwuvhqrI|d;!01nlnV~q#3N{P+O zc&eN5u>r%eRUm_uufmc-m9Xv?)5bog1|20YpKq+05*1>*#sZI)c$Yi@gD&=jf0M_HdZng8FX)zu`}Kb*Vap`bP0nq4MJ<&;_bVhUi2ckoUfApU1aI zb*^eq`tA00l9xcMJ>b7L_6KL;_JwQ+aHYWievCAh!6pZChu5|LxPtld|5!RsxqzE-`q|x9HoKoVGCr{zf26hKCWQ!EC!c@TGb@V_!U!%;#;!;xGp!6(N7f51(imd-a$0lqa_yuS3!nZ)4U(oiwy2bQ8cbGrSoPn>21OH-poT;S%#racm^r?JjRG*K4Bcy#s-j{% z<0^>ttM-68P==o%{sHiJcg-=!(SJl66XD$jS6-i#SBYfI0jN)AFU|Mg@5~OsGBx8g z|Gm&;EM;`LKMb@c$^|kH2283BXYka3u?9ApRI=Cy*xXTtp(yAqVJ~A<r|uRgb(xB%15j&!V2|D7Ztl?nF%MuA zJ)A5^VMbAs6X5)0%qGt;;|G2h_C-z5^{3{4=YA2ab0wn`Ye^NY>MO3eR!?C=-TG*JC561aJOo2YNlDtsD9?~# zXlO`9U;n72xKUqB3=YO21IUcz0va-asw)d1XtV7u>@H;Jb0(#b!815{y!*xer=ESs zPF>Ewe=a~LEK5pS+9sg15d3Q*$qJ@VJTN*c1%Ri2L`QTd^&Qxn+a@X)_uu{J;=F`E z9=DP3QtqD>`DwNPMzCz?Z=x`RC-Zp>-MGRfOs&>tt@HJH1JKeANMq6fN~J}EiGW;7 zo3P#>H+Z^8k@sT5;0Dn47#JV#hiQX73~Mo+kIa4hB(b9mNM29;C;QsM)1=MH$id-$ z&&tGvdfu<+==U?Zb82epeu<)D-N`>rACU54Z`sD{Z@xH~?+2u4e3lY{^)@{os>`be ziH+BH(iQqE)cdlGYN58aRz+QX`}EIe^Xwi#MDx!y_$Iu-^Y1Rd({@DuD!#ICAVZGN zl(A6h(-fFvV+U&Z6V~NAlNcKb=;^Bdyr8{&yxzhF^o95=&XWSAC9|t*0qs`dhkw&i z3+L)~t-tB}h%297y|nega0T9`M6%R}=Qt}0<<-vTBS4?$XU@~TdA!L<%O1s!=RP2z z;BmbXE>a}abyz!`qAQ%kEG$19&_Y=UL_IGTw_05f99BK|T4r?G z+}Oqd9Z0>0D+SLxpf)n?6bXvA-OIHD2#?#{W4{~6ryuMsM zJIBTfIug_@1=sTHg$^lsYTg$j;=Nej;bO761(nt1-D!6gIKWkIO2=nW#J@4v(?ne&(ZH?~m>TRWK1 z#r`(J9kVszn{?FlL5EL|Cr_tF+}T~Ty^jU%FRJ|k5DM6F1`JE|jry*$8VQ}I)tZ1(OA7G; zAt7OchL}+`>#*~c;>!r69=ezx*xtBf-v?SovV`XM=%{`H-1+$Xrg&#=F#oZNs`#*` zpQ0B~rR{tu5$p2>8kU+JV-ZPYeuZF7m z*U@fIe?XVQF#$yE5xg!ZDfdqfG1>bwpDii9Qs%x^=HTr%piAAx;}1+Mr^UoqL0>Q3 zxog18GkbT5vo%Y;0F=JwIb-gFX*<2aQJg^Yv~0whK}bKK2@;$MXUto?&Nb&VMfQAb z62k!{$s0^8CMJb3&bYX{*#Y93REvf$+U$QPbezCsANv(Nsb|In4_Iwh+ZQ>1yj$}L zM(1dsFjW{#U`*uM+Z^O$n+MKIBHhIDa@Yc%`cy^xXWQq)7@+EPKKG-edH(Kh?I{k> zJ^Q94a)rTr&L^{4;W3m#^Mz(7HrCk;G^ovJZv%`Qki4%9IX#l%VjIGfvo{5n7`wQ7 zn#|Z|ZkceBw@So7jn`+e*E15cLHiX5kTV;RxYN8tzy`TDMw#alymJFhs{?;2+6+7Mn4tM8~>kICcqan=OC)6bv5E<{FHMNaAfj-pLK806dcR_3N5@8W=^RXTyw$(>66sotiV3-f@ih7t-QvVFhE4MU<6!!tvccRu549x-a$l#eQL|5#k`qLivK^I;D zLdjq?^vECqbTW4Yll(kq$x*#sGH;;{L}**qRM$PiRylu{XVS>~q{h_3|9m&tXsH50 zC_q4hVG<)-???Ck|CPGW}YqoI~u7#tfH7w;wD%zf(NW_r!c?z9M9+!O; zIyzrKh`3{}c>O$@i<$ZV6K8?U(*E*H0~DOS#W{X|uJ>>Da>6Cc4yBSL0A6aXJmB<3i*S)7Q{or}m?O0g+L4;wlSf`(U=D%}AmOW%k9qFYTp_n*&= z0}HFo8#;)#Zd=Tl;-u7$gTS>EYcA7@84xCR_RBl*1;g;pMzI0acjqGvEWR@Yo{!0U zl2Pb*%H@f&RNP*{$+dNwP#o|Em`Lf=DS~C3%`j$3P4t20Ug0(q`km;W>( zCt`sLdnoHha$QqB(N(H+((OYG_n68`tPQ^7`=9+<#-8j1dOIkY0QLL=T}wtsR`E!< zF5)~OonScNC_F}|0xajX?TT-psJ8W_os7`X&_EYLZrfKIQS0h*%x;5L#vGR)#_0Om z3cQ+#{Wl!04*i`rhVaCc%>f=&YNq|H@$X*uXZ@+%wC$;Y!kh7r2;8`M5xN_dU{yTN zflq;#)X4XqLct1({$|x5)C>ZI_FCyxbsU;%o_e_MGN+%l<|bfjXZA_w;@a8Ohka|0 z9v!=VtMN8MiAM}$JAfJQ%W_}+ea?NQFxzP`)djdB04aSv*qb+K_`@-r=U56GQy{_M z%SFJ+ffgmnP0fG)78rxCXbWIs{A9=Wq=0keV`>P!Lvlag9uzc};{S5w*O}@=Pi?vI z?UQT!Uv+mtv;38)PqP-@@n`pfO~60{8awEU|Aa$p>0Wmf6q-*eeLQ%aO~2h}V8>)? zu6H&;hi$XBFfx|4x?^^eg2t}HAumIjq|aV}a!#C`X=(y_=@!I9AkT)ahIsL$)BDOU zo&<~T<|jb&$BWLcjLcW9*MwC|N0+df9jbeqP?$Qe@2*d^(kDrVR)wOJ4Czj)RKZ%R zBNmAM^gRSMt}7nUy|VAPNKf`ajc(VujFn;d2xUzw1xc-ZRwvQmP_606w9N9s8V_FM(;`czc;d zeyKol@-s^!I#;0go)XD&E!ztjt`(2e1Wse*_ZAaW&>T;%&qscC<&rff-Vx4DeBKOJ zfY9;0uYG1g74*RoKy6(SA)cNBY9BuMZYIz{$HptE33m1=uLhvm3N(wvNLsYN5#-O& z;(-Pgccmt1{GJ;eNW5ZIN8|_S6Hw%{ z*QO|CJ8$2}Xx^!-tFHqmuRI=xr&*%HWHy#unyCl%=5{>N+4 zlq``ZCEEf8LV|f*+FG=s>6?$WS_eRoBBi7Anc5_i+fe9=B~Qs<3GDJ4IFJ5$3A8X+ zBu6EURJfn4Ce-x-VItKcv1wsi!3Ok2P&Ks-rQDF;6*Xk%^9Mu&OG~CW3Xw8lt)aRK zZx@aU>-Tto(V`Yxu~b;>-L}Oq`W0|~pbe&z?VQ_r3 zEuD&4U=?qn?8*OrM z*Lg34xLCnFzXOpeN{qw2Yf;#o8OFtG563>_p~;a{?-te{O8iah(}jQ0DLDm%rRC%U z6-6a-RrKRc)ch+5aI3fR7rsvf5UZ+8ocTmnzH{jx9kP5q{`XsyWilL$j4HK{KtMHM zPd11H^NBHomZ%wuhDi*qh5$zD8bBnoT4Tp{Sl@kR=d9jUId=PKrn>8~%*1{ zRDBw#1_vOfEWmmQ77#^C4N|ZTpUp%Cdmp#-(~;Ht(t5Hd{X5njrh`I)04)^{Uom!7 zR!^o*fH$aGe;9lTi0&}raw+>=<3pxuP?&U7cdAPitLRy(w>?#DRom`WZT&YYWO@jd zmJEI-H8lD^92g$>w7MPXLPfN#6jS|QNtSq`$$+o5YH5b_u&zGL%>L_gJT4v6z8Z`6wv-w(&$#fPr5DC|0lbf3wjRJ>Tu+vz8pfo)Xg^cOe-RJcrsoeSTK zd2jweHAM%r5}{RTXY!`<4QCidq78Ji&+NW zZL>v5j@4(5Q`xhbtkl|kO`(H7mzJ{`9UIe}lN~;nC(?qgB1j+BF|Mta z9aC;Np-eVU1Mnu6y9r;qnS{O9izn>k%IAl)lT314Z;o~Z%78C ze)|HO!uCmRp>+B{r_c>PN_;>Jsu&Gg6%NQ0N{fR9jB(@Z2q$cm-RX$i~J!iZbj3$vy#p;AX$TQicx%1S-%{$e+tTUcfwJQmVjH0JB7+ zw2qU6Zv{S`B4iMHPNaVT{u}0(ETZ5$5JU1ga$IdBLWQ!>!FiZwooFfKkT3`3!_|05iIuMW$;e z41Wwj?9j8_bPAFbV7|o|(w>!bK(A1m-vbd8=SvCiag^Qx@*u*MO}2XDfDhPf)=OY0+f z{Vt_^kDA3r=#t;;g3XDL2pbZh$MpzT`w}(eGvGC@cRSOs166V%i)dIf@Vfw5Xd35P z1bqnuO1tG^ji8bW4>cz^TnDC9bq0b%(OciHu963Y)Z;{ilr|?G@dyah7JsNKRyI2^ zvVsy{pMfC4IF!gg7=F-*ighme5mAOC-gxI-Sc|KX%9PLV3sjktEigYePA{$SFQ4Kef)f}m%1~TlS(xA^`8kZYpUiQ zsTo85!6K&PW9p*Cbd3Syh-nV(Iuw{tL&*vdd@C+W>H5&N}aoex^AA3Fd-M*nM&;37)y>(R8&AT=(xKUE1Te_vYk?xcbHiD#dcY}0ymr5fN z0-KPKE|CTS>5!1_`ptggJ?HnXbKdn`%g28n_h;swxohUWuWM4?^xh9R9(nL2YOJdJ z)eu{rVvoWsQJTIK8A=~_`pbl*)ndW7N*3K(x?Rv3F^#(mFg+~nD#0-V{`im-J=jn6<_?DVi z4YSW*JTLdCV8-UESy69O7ske3QG@En-1uts%^4OV4@~Wiv!C^k$Bzq(K*#$X<;1}S zZ8usVupEkyL5lOb4(@L*T9)N#jHnQsk9k)|&%K&GhOURD2*e4YET2odeg(@`xm#eh z8aiS(^QL`x9Sm6`ulQDKQJ8NKhr8w`FQfO>V`5NF^7e$ARM<+I0NSx9KA=VWuo5i{A6aybdVtFj6TY|;yub(QS;k8!G(((QGuhNj2-pjwYq)Nw@#_iX! zmQs$WqgC*wog@d!mu)JPcm?@C)>@ySl035dTw=WCv$t0=b8dR3s}X=H^#Ks z|5Y253Jti%Q0l9yiv)nlL4=o4VnMQOrKf!eSd1Y^b(L)FL%|D;x zakP$sLCh=7hNbD#LNG0v&w)hz$DFL*Xp?vcoyB;%Z8cA)G3;aRI`u-V2+tG|pTNsd z5rBD=!U34o8GF(T&{1cKRR|d5WjN)dZ@Uknli6)31f@_B*NHP6%V^ zJoywiWRo*BR!TJg(X~}iH+C;ebtSdhcC^?KCrKlJZW&(DU>ze3gKYoFIBB4Y09hUe ziWGQEvXUVa{b5FpwLS-3+-$UF6U}l{H3s_HE0<<$3;A!^hhb{!uY)bozDub@O>FU| zPjv%#RGFSDN=$A7ZiYTz;zH-U$W3ZholSD#HKYx)qK9uN4=(n^Ry_O!?l=n_b$eDR z>L7PBPvczk2=Wa!uV5x)rBafJl+;Ui*CrbWdGQAK$?-A0vGVOh|9lZq!#d17RCQ6F zLKm@RF^IPB><{E@kEz3?;Wm6{-#%QMED4!m2m)Fh*!K@j7#Qo9s%MKoNVA>~hj^kY zagqA_r<<4kevA9`uR@!2$H!3!_ESXKbtIUxoizdB-4rB=6y<{YVCCJXnNw>78PU$I zR59&>tDCe*e4E=!T#GZSu8NR?pdr7@pzP@fDDM&i{sirOpVlw<`2K8{CA6fIKZ|tW zB_#c0&&zQ#UW?w7>n96rD&3d2T;F)<9y`&h=|N)}SC2=j_%Ms#|Rn1`TCd`k)52%TPQg-#6>bmnn(H>1U$cRh6{ zJiY8488|BwGH>zINdAnrrRLSSbVWHh6_NlC^T}i(P#1EY(AyYnX5PPwPhwg33ADS?jLqprytOpMkdJ74!*DSYuQqODkB)9~< zc^)FTcko;t&2Eibh7&=H<7l3`!MiwG0y{;ENq^8l%qwAdBX);Sz#pkz}J;VDOW2J3)@_uQ!c7OYcnNdrCKzvP7r(-xcOM;S~ z-ENU({$@GzkQH|QGR`B%k8@Mr-*UEmjYzG-#O;{_Z3H z<`SUI9uvbKDR%g~oH#Oo+HiXQAYL6iLM1}&hrsnBjXbT!l zid@C0SgojL#(uEG_$D-yB>h0HoglE^-Cm+2f)hQrWdB*E^1V(Z+ajA2(N&8z7kAES zox46#>dorA4{?h2pW^-y6gHkPe%l(3xNK5BD)~Lz*B9AK5EWImzN4{ll@j~YMY$($ z|By_6{%Dcd=BDfc8>@iqboDF|3NnH!Gi~TeZfv=CH0?k0l1;jHA&wqRYuk2)v;`^Z z#r@T(Tc^(Qt6lIraWfUdgaO)fweRdZg8y(g?5fs)^_T57n z;4;-Gb-3}XPoLcdt@Sw}wbf@ddPWMV6Qcbw6FDvMtZenE5w zpUA_OJ3gRDpHR}Oy%eegJ~J-%ID2F2^ApxNbx-9r>)YLbKiRYdpiwlRrE zh_X3<^}h^?iq4f_L;vgqc|JT=ph37rhFPT6oDUT?UvcnStK!Jov=@k&!HzFUX^P9# zUNF7z1V^)*f#r-Xrx#_5xrkU$Wfl1@&s)x$v%P8w(jP_os4J_1?@jP=uK~>Y&bim1 zsqyaak(|=mcXs772cE;PVb=GmC>Iwoo$1GyNP(B_+B8dH0oNMzrQa86TWsXS#SnAr z_2li)Mr8oXzGMA2Mhh`dah~^X1SDEfoO7WMWB2uqSqld%mRAu$VB5mg!`MwN2F2TL zz9W`LU!s32Iz&_@(MJD1HNY`~os)+z3D#MlUd0^UDk?{jw{Cxb=8lVJRyQm7$B*%G z1aV@J$F}!9+u_Al9ks`88!psq80XrNcImgL^6(wD6KIA|lg+o|{V|m;aAVOHqlLoy zyZbhp0wpJ6%NVq+AZNF4+oEw;Or{HxWheRy7NK9!9mLKiLLLZvjC;M*)^k{xd2ZV9 zvY|l%gWoVn@u$0}7@kf{-^pwWBhvJUZq(d1VlR!rS|cR*u-bA$GYH#4N(zK z#fsJcF>EGK@DpMITKd&RKeI4QiRE2ri6Z|L+=#|HyLfWF;UpbCtElZ3RFgb9Hwy*B3Qg%(SskLZG$qP& zeR=I%@i{l3c#);1&2*zG@8%Dex)`c;V!$OZfWi1<4Ip#dt$rpX?WLP4CYzW^`cEGM zQjw{!08_eT!{|TKwjO%q%rj95DDw;VLfpg3U6z}L?@dN<7oev!u*@~OH+MLfAQ=)- z)1@+W5=|>MmgzVhpT^U@mgg}4WTfY@z$)XmmJm?PKoCc~+${lmY7V|2NrB>!D*!*8 z;419QR*F4?nX4~7tD;3oj}Q2~g+Jbrbr6J{Flch^8wL`X0@q?wt&E40nalDH{e(h% zDXU33)|%@I{+)R?(V@yabi3>ymh|o|iTv~oBGW_#by%#Xr~?C#J@is&?3c~aWYe&BV79t=$G2lJd| zkEvjKUsNUXNOnqVMjI1N2_`bRX>ebmw)wRYdN=4D++15&p9Nybi8CXpmM79UhSHd7 z)Itcbcwl60c9eaNi%};p}0hz$|I2`YYZ-~o9 zP4g)!?DMih7WUck;YA+gMFXoysKSdtPZnxh|{XmXe?ysz|Et1LvKT^}E6p7G=z-CTM?KsZZ;ji-WL zktb|MWLC79?nG(Ax$R|s8aASA|;tS%BKnJ_lYj?6#Ux@ zaNx~S&Um(FTgt_5_$S1lmEU+5>)Aw#0p9=Y~e-%fg%Rav0_R|nF^exF@nV|6;jm4teW z6G7I?YbI|q^vPF?+()e=XE9MOX!4(jWiHlzS9G#;xoyOu_!b3!z$PVNo{y?tn3gvw zZB^6mQv0~k4FQFfV{mRv0?WW1DjJo=>3NzMfP(<90J$zYSbqN25Yqycc+t)Z+vZae zA1VJv59><13D)x1{B6Q&w{T%3`9*lr>zOVE7OTMY&X~Tnbe5huN`%^bA3X(zRNPh$ zO3Z~3>vy9`D&xkeJBi|IkmNL9#cNMogPIn#*Jp>)o@(K<(b9D{5Q3b zY`h{7=y&b@H+_q2pGCln;&@LyzhHs0H6k{&M!06N7?GzeKmtS0?Ig6gVibPf=f>>NP8{glYLxu zW|{($DLP4lc=FSy1$9ox&wakS7WD3px^V@A6-r*JiGm{&J?4`c^Ax`*HbykZtxK9E zL!sQ%ClFJVkS2YPsU>@g34btOSv>sV32)B-|OfFu#Dmw zfA7MYyurq3k1~$P*jH6+= z6ekQHY`%$hKyq5BjuC|rjmB|JGqua0zF<=+-XE!B|FD>#A^il5~G-SBr7Z^_nym>~(oY9;a1DZ9D*n3_M+ zCE<9wVb{cWg4&|?aXsi zVe&ID+_-2SrG`3*&bH7WjoRyKqLco59cO7oPV~985Jo*FS3Hb3eoAq zPy4A4AxEjEbwwn)zmn1SxPtplB}CMB7hfy|lC(-nU^`5PKj#*pRp;Z>LrO!xnZq=@ zIKh}cCdt9zY+7OxRgbw;-T1J-Lbz-DH@`cc z@E7MuF|*dKNUfzO?+JJPOvOgGdAl0=yqc7ANz`*mjB|~bjdXx&MO}NYzm9Um;;6#y zi$eolpqT}=_YO#v%bu4{)^4%Fc6<6eOIi*MzuG3`p4G5nhYKK5eYrY=>CX=KnGtdD z`Epo0xVrK@BWilKUN}kUU@s{bD()}+eMGRO8Jx!Y=@=BT7ZfRgabH}d;LMzAhRAY7 zv%(Zm%MZ^?E@4Mgr1L)4rFTU^c}e0k$bFr^EwWyBY=1(#W#vN8QYQ_Ed{YvCjSrph zl0JJC21@~LPh*nBIx{-IZ9Vp#}y;G!ZyI08o~Lc#t_$DSW!99)%6qoW z5#{AeZR3Hj_no<)@L=}Z2BT~J$7*P-M8~~s3J0f;yIn(ds9`}lpRDahqpN_q%{8P0bg0iH}}tP%%D12 z#~lhZPgx+lh)TN&`!QE^0y}VhRQ|Ux)DNvXs!!)9gEj}rAZ>a~u0X~epF`7Py6dJnpA)BZXDb$W>SlDkY_7Pxr9iIl zTdr90YNIvH7%d{2jFXMn5$6eI6>#Xt5ODozciDt%_?*@0=B6i%*lHli!^IvCUs+^h zY#&c@=Yuudv@_@%QeS@;r!XD+UBmZT#`Ak-#xl1Pw~;}CcoExRdHQM0rTz43&F!5y!}@dzuFhS}-x9#D z&LJ=IVIOfdd+Y*1f%WdAU8I(#=)&Xsp zLy}7#FR*e6FS#-|=n59eGBzBABSlhz<7%C-57#PcKg=j({>|Z*&j$(_Un6S7;Df2v zRO+cVyR*%n?$9|MYES>tPTZEm*Qt}D<&~Nlp$Hw4%Nrp1y5Dp>Gl+Rd>$r&drvm&gd9RBFWbreEmiwjqd$Z z{=Z&~eU|{%hwzOv18C2gp|5hBcDku@&e%I}yNz@H5%oi$IJ!L{E_AgoR{`DoJ;ydr z_;k7T8P0>iy1eYJUwhL0PtQ5ZMtoQTo_!ma>Pkbd$+f*^4udH|V*Yv`S&jnU9o}(h zVhvU#>8b8lTddTItZl~(BW@~K{_xwEwxmHKZ7=hCX)U*==1!x_-+r0x<7M%kkEDvY< zi&c#R;S^k8Pt+PKgAd|h#f!XRt0b)09Yig1u^QE%rKZL#7s!};M|AY{M#*-z$-nMr z8S5#NBIn^lb zChK<y6V#e82q2^y$9(>MCDBV>+ADfaHBtUqY<7+oXr}S`xOVhQ|R=YS`>TK*(A{ zgcCq|p8o^^iyz#6^XEedXHWG%fM)-C5%w$7_h>GWYX_^Jb#4O-*>c2d>*b@JkWe|p z-TZH$fs^-4J>iaI2ZuMe9yV_06kceyn(`M^Y=pZJMrU%^>5&qjgCh@!eP?afy)6EQ zpd5f#Iihs_F*DspOWt3vj>GBbicRw>TlU174em?4SKCa^{d{g6pS9j@(RkeoJhXtO zR`&C0KZ$LBU)t+1dnz!~Sc+JjlMupOY5xow_Qr+FESA4V*A!OIZbg1gy4uTVcZ+9T zXEA25r6w=_O)HtEi6Pf~*H(QuZ0`oGHxn0{heutD?7M+y{N<`2#G9is-LMbgtQ)f4 zur7A1qKHsny;6S{T;tW!Yc0?H4{l>=-!8z=ey(L>P1>(YE{k2$0EC6<;=qUf(=#26 z-~AbcKP^IQeAw|7wNj8iiySl8-|D`bi1vWav>`OTPLGTnV0x*o-Thus-Nf0)M|eyl zN!^L+iD8RBX<_%|7GDV-{@0twl`Yi*l})A66EnmWlum6*20uquIVG_SG#~lmxGGu) zpJwdIQzS!DE3=A7HN*f{OaW=AFLUnsJPh#9Z(YCSMGd+m;;e{I~>JhW!AG4Uz)gYO-(>!F8RSYYNQo63Lb*r4E zr3v=dM%OE6UsrK1d-QtGVotp;%RhaZJD<8EWwD9L>8(Ds-RT>R8t1RysDDFB{j=$G zvSHIP+p8*TW2^zWd?FcF+Nggzc@03lp+WmP>u>h_V^>!0l<^+zwDxtzU;kVQVs=J% zZ|?WyTJQ9G#ziu4^bK6~oJNpn)kD}uLmbV5ATp$qx3Bjd9OdB-YUE2Gky+rtZA9wz28@Zx(I z|0T}j9rC&JfsvHF{!MYqY9yjp5d*rhS7%N2f@f#4n5hMxJKWv%km`}n=>b%2!-x-$2Y$d_q1F)76`XuTDx}3fX>CHedSVqAeH9VfItj;-IBDm2Comjr z4O)==bQhFG^m#MPtTv!+bz2*qD3J&Meiv&CQUIN1LDVuu9XXFV=6|0C#6IY za*C>IcOT7ZC|IZwO3&Jg>6s)-UYozC`XN<9+Xt8^spO zjiEB%H7uuP(zeA2h&Z@()ujCCR`ys?3#_fRQK`$Vp4O|Fv7||b#gvrE_%LF%3OE zMXe(?y!dQ6$lcv~X0StY=5@H1&Ns^Q@!Vy-g%utqMj%#JR_s373tU75^Lw}ak7}Zo zL4>OourK@M=I{IsInBzY;w>WL_d#2cMr)VqLf1&?3ppC4J6wX7?`NJ}GFEC(X`OAU zc$P|I+2NT@mM}Erzn^EaszB^x3QA_kD3rm{n0#Mv6ywz|8p@yb=18;of#!P+-fHtW zy@&uDb((L0rSLVDY=r#LB)r7_OrCM-Gh^vtWO(xDQcJ4bZ0Yw}+(l+IR9{~sj&0LY zag5r9Xe(@#?^}B=4=sy1dM018qIgv#in}WF$P8J5gV-HewphKm8D<4OGl+l2W<`i! zl7g7U9c;`!5No2Ee9Sz-RL_Y@t1Eu`!@ z9&B&YCQSzAP~O8^skJ^mO`A|-<0KwV8F+H6At`G~f*cK_kg!s+1Fthl zc3e_s>3id+l;Z|rJi8-IA~!c1Y{c9ed`>Hy#Qx%}Oyrl^Da&l1c^oa1lH-H;_65sI z$+ctsfG&-S$-a0c*MWyC%F9cEFgt(0<^5H}3-FF0&}XxTERCyXc6uq1LX?r+jZuwcu6?p2R%7qw|wR?>F{E<=6KPoD@utBu4yi)JH5N3Di!!H^< zAbFWAF|MUT(y{pt`pd`=&iVGw62Pr8mRJ1_c~jsl7fO0`!1X@4OuVui!ViNs@SP~f z-fVoWL`RQ`Z20(+&O>K-E5NrGu&m0@m0M!WbymON*;xaA! z@2{_Ck{Y@>7!0l6rHRQ#`5_kKNfX|BDe`q_VSbJieR|t7E};z6kb61u>j1D=K8r8> zUjPOfg=nv$e%ct?)?GlB{xvC}AJl%BO;^y93?8^lvjrwbBFYcj)eu8Xc;7I`cOK+BT;e)s_r$oeEK=|r|mOjy2HN=Nx8%90Hw$~6WPP-!5SyMGV-eo2sf6)JsIHsM z$re|CCEC+EI@LYsRT{&7eI|zfr}1+B!xHC3HbA`tvj$`TRr{~pd1Pj{HvKL>1<+jr z9Z-hp&YD6{6^|Xxv-Y4Zm3co2YwZ@zII(-&K>>NmS?QmVQDBQWxX?_j8Uz%0`)?sJ z9&={A_?zGA^S9>SIvpvgF|Yh*EhUJ1+%kd&XO7WOS%X#C7si#^Z*$Z6jRXW6c0V+d z1DnAV z2}GIRP&x{Sl{L!wYd2W}Rg#Z<-GdFbe1Yd1P6wF zBr~A>FzK|CNk@jOMfivh$HShLfDwi+x`+#XlxNgbobiDC0S~pwLpUaNzv1s0 zk~05_yWVsKFI>KnL2Kc5%0g-3ON)a4!2aKL3BU}?rahVh0in3zL5 zD=U^CyjTw7u=JJxZ@GZ}JQ3Y^QIYwyh7;v2@BoD<1k72{;(nvAh1uK6qo!ka>b^x$ z`*9GVtTrox6%;4}s{$Dp`P;aR&9IR{v!Tup=J zN_C(tzu@9^xTyZ49vP4zcylASgqx8OqAZ&GY6ntrvq!qYJ?;?cT~gpz-ePOGWbXe+ z1`g+nIIMuODjx-Q|HNx4Q+jK*(*MV8%|mQ<#6ap0|D^(?1p{b*|4=ttXFXwu$kh`3 zJinCo5QGi-^govWh?OM%12XUb_@A~3iU0Efq~?PkLLyM8W#a53fAacS!3GoHIvx=Bf zh@Cy~VXuETb<}r9JSc|H%4~-i;f%F&lML8*Ptcw6zwP_3SUtunLEg^Fo2by@H$6+I z|HH37J|qJ2u9T4mmFmDQ=IgHzPcFs2-#zvok<_R~#1%?8{Y6m2mRW7&OsI0?lGoa5L-abpnSI_x4@av_R$x1T; z47IYyfPF7@e+Z#n>NNm(`_vFE|L=15U!x@bYW~sPITh1vyv<^bv0Eo#0H1Qp|8bGP z!#PxQqCk|vnfL9x-S&0}U_I&p=Rc>zgXCSibj~gP|7c?tWcfgQpm#G$j0j<3+JNy|VzkJ-bpnBFYsT{OG{+U%XAO5@{7_j{X@(fV)c|B#6Fh^}tK`^z)#B zpw;4ujj-n`L5KVIXYb+PeyJRG*8^IqN0LQ)SV4f9$q{lNWQ4uqmX|1$BA?Y z9&zRFBtN8{{OXzN{k4Kl$K)K={qyUOf$MkYXKoBV{ zms@fi^nksE)#$7~y*ORcQ+ho%-CYnnJko12?Qcg)|2qO?po2>Ui2vR1>Ach_T8iBM ztq#aqBku&>ha}J*>;&4gd;aEOmC2sEK@|479r&Mz4oHCL$je7OGza$`0K`M)&&I+c zt*jigy8rw4@7R47e6)eVK{9_;IXQ6!g~-JS0gnR;IQ(dBeZ9b>H+E~H6es)obTY8^ z)A!Srm6iC^)QyYL!`0qH01saQ-~(}OZG07>J?^!Q4G9|;Ha(=Y=Ebo*z3Y%o`?lLq z+qrs5E-uYOgI(-Dcac@8)gpMChi1_LQ3JkBrl58BRWj^J*+x$uM#5Eg5VOXbfL+DY z88;&gKpD8n1k;0t%Rc5@wW=5@&=X>WO(aH+dVzNr1Rc4BXdux?4;bD{J-galA7K96 zwV5}~`oeOtRmAPj{O^k!fHishtEKViJWliq`Qan7hDNqNd+{De*t)72zZ(|bj=UhwOFQcQDQjaHY+Wl*!!bq)UR}IhY-&K z!U<;(w^b?@SpE&)?F^U!M@GNCSZapt8#KA7zgN|eY5@oEW`|3FId)hMAz%{oKAqIz zFdri9-K3U@ZlAMnGYA7b2zTN7c8%TTGY)p_UMFvdvIWLXy80Hh&jZWK%90;Ok$dlQ zl z0Q`SH`Rytk$>^NEnd|)oj;guMz;?~^Zd$~jHz0I%8>7Gu#|<<*^N+D4Z+Zq{-JIz6 zbba0fQVFw)%ZgQuf3@8Rw?hkXY19m#=#-sG*%YsS*4vgxe3FV2y*cO_v_L-Hgw^Uir+$=q@=+JfXxkP`cxx#P09V2J;fMyE0atlKs(Nr)< z%ymsiWF#sLE$wm?TWdSO*#D8(2M+fe*oVp+n~4&2kJT7H(mb};ExHk=0%>56$n0A$ ziv~wbEf@1&1jEt8Kq5@tFK^Xzh@9diJsGS& zCV_!VLJhg{u4^TI-u_}6Ily44ZChbMv8%0q?u?_Iz^q~z~dp!A4e z>Rw<8Sy2g;Kue~wwRA-7Uch0f>|UUK%H6fFE_8f_bY}ay?~gg(_1RvIv(wK%-{%|8 z-4}T;_lM?}FRy_W7y&6}jg96Anq7B#ymL@;M?c6qZ`P+%2sv+{0~Uzxo$)4D>x*lP z#YbKB4mHm971i^7PDwF2pW->x`}pqxiv=myY|qX1SYgy7QM^np`5;ASoBkSZ&;jI* zkA_*liKJQSK&`ZMus%p>DW#wuzm#~vB_5=>f>LylOhrN4vA((*j6>f#Kbh$IEIfjd zuFO;)fHfP%R$f_;7hxbGB8s*RYo}XHmYrU<&7SeqGf~fNPk+XAdo(aQdU!lI^r)70 zj+oDGC?er=9Z>3{diJKKCMS!uvp)+_fP}>V88Ip8&E@>7sVG1lvA6yi{Tgw?KG{@zI59d_P11%*wd4~u_lC`LM6gkip7nK9Za0-Bo z^KSh0x6!uBCM$uTJoLi^{;`=sJ* z3NSA3D&93$z(@sM7=V!_ne0k1+uB4G+F&JBbR2anKf)mHnnNwy8%d-(PFm7YOC?AV_0^36It`2_*9cXW5m5shx*LrrWSufu z^WE6&lhNhXmDAkZ%r=&Fg|LtiiJ6(%sb}==fX-CZk=alN@*CPgXR9M-b0r7%<${u> z0VWK#i9Fj+Nncf!>fc>YLkNUwUDx`sDk(yILd5HvT!9|gFe}*jX`?vwC(`B_ywT^) z;wNCTU>HJ4z1dsJPE$9}2BUii$9#f7e;5NV6MPz4KizCnXeepi!{ zDPUMF$E9S~U|76DVc|?K9>nY}Be(#zP^CC7TZapQ5ry?~&wf{wK}@V|WD;!_|4S$a ziJvHe>+y#2AM{k4(-*U~2IxH3)P{GjnY2${+*Ksn=&g;st7eH&+G1dW=~jmW0u{y_ zOV5sO^s1u^`^8qg7Ov>8Bibi3_|o;K8YR_xki?zMqkTPdBfvx1LCegL z5=wD|Km(J?rEpT+f&6)N8*CVR5Vic0X^b@KC*%FS)ThUiPkd4yps+@WQT+FH$9yKd zgc6p)W#Obhu8a3#YGwep**of!Zgo#RQH`^14FU3?Hk+yMjCAi2;_A)iakj%?X|>aF zP0G2`+!bA0VZ0WqDc-{rTZtfMJ`xEnT(jP)70Q9s;54$&6-&nJ*M_Y=8Ci3J*R403 z!?QgP2d{434M};Z#32DfFM6xjkndM6269>0tM}4u7rNw$fl zPTW^dz**f{KLjS$$>Eqno3Be5&1p8^!z$Qu)%R;81xdqiF4Lv}m2&2jULB%PL&{{4 zYF;e4{|!fPOCK7m=VGk8ustG0TE3PxIrT}uS4f}kUbr8vvO9{v*`hcZzM_iI8rviro_)Z}BwTBQwEcU$ zxa!Ln+-#NBrzp&VR4zI?Dy=_fXA9In$Owc|vB8|0niXH+8}F$GDOO}qwslD?o$Wk$ zE;7Kjq*?fuS?OSX@t7XpzaMiFC4NaGCr2fkrlSzYl8XK~B47fNRx$I&xC|tndF9*H zK~H*mdZ69n^*_5&1=%?$^sS&JMv~eoofXMntfym}gB44@`rT%_(?y^5fBNyR2tt4( z3)0rr#(vb$s_Syoxr$e{?e9|5<{F*p_p#qErJzC$7Wr&{%8@bMYVA^ zYSnp$hq2j8t)7gGO#Og6m-$ei_DN8R7@PWgDc-4plVRfQyr3v8W>aQ^nh=(Ba@Abn z<$9d~KJ8r5ogw`x@8gZ(heG7Vu`)qXkTk(u>KaNiL_R~ZZm0X)mVh!m>tILQmy182 zlL?MRzJsKFIm%!m#)e|6vWFDoB9x-AYSRQSOeasyFin5bZ=le>+&^FR?*-_380)T& z&Xn}G3bt}6!{XY0s_}kp$}=|=#d;qB=5HO^ou*k-`fEp;4jhZTcJs)*#WvQX?~!y< zWDDqCmd1ZdR?g%`0qkiCBKAi)@(u}01LF^+f@x)!%$Km9V{XyX!+ba@hu!+S%k~ix zK*Y!SuZYjn(H*IY{He7s)f^KU`GG!4l06_R`UW%$>9YFhXjcOG4eh>kLPrOOr9bt? zNDONvI1WmE{q-W=XG;ss?sb-bF>qw~2P*1Ivu(q&N z>y?p88abEYY+1S*3A@pQ2Cu$f7Mq*K#>P)^al5)ZQH|Ku>Ejpi@e3C+TF(O>I-sD-DB6<@`oZ3r~De8J^f4)82W(lg~HOu-!dpY_ntg{v<71 zBL}#t$=Tmsa@bJ81Ue_qJY+d#2b6`KF15SUX+xux03L0cZ-ax(B+Ik47AAm2FKA87 z=i;ETrR9BTzkK`TCw&5)N~rn-`g9yh#{8c)jh7A_-bxpJsht*|IS~3IeXAr0j$#ac z3Hw|)6#C!`?(}?*8qO0lWE4`Os2zyYczd+?OEX1+(KN+ajXrRP4Rg7{#Mgt)8rME- zUk}ZnsOWoRzxEwAMMp|o%dsynHuN$!n+}%)=tU(wECc?w+3>+%IVsuM=+4f^Hoob) zgJo|Sk~!%nW>R0cRWdg4dLEfp)v5rX@N84$3kQzkwFb_b>7F?9I9kP2L zC5{e37UU5r?;tPSft#xV`A7%TJq!axUgjnTcemcK=4OAt9K04}bo8~;!98rFDFH^t z2z?}z&APST+Vk16QJ~0}Zf?xQ8+&^(XauYbDa98T7o>^}7DS1k87}IqB5lyi`xpjk z2A}8;GwW96=lfTTQ8V+`fXD@9lQL&E&lB}snN48h`iu;rd!NN?TPCM7dJclr6>dVX z9-YHpx8IFlOI!BRb^ymGUh7$|%-0LR>z8Jg^z9}rVZV~-Bm0DnjM^NQAw+WElm6?F zVF2H%M61o^jg$88{pHRuIVGjFcW5|{jXAYrAu!xi4lXX7j*gD(1JfXO5fL(ce0&Et zx6tBZi%NY8yc}7DTM$whvv8y6kfFZSn9Q;F4Yxw&lT5Q%AN21&Mf~n7!Ux`PjOykl zJrknir&c835)p7?5d;!5vpAngT9ALaFzT>kCr1l0Jwm|G|=ZM4gxmRc;L|`l^OWu$83X0Wim*m@M5so9Z?sTR`CSi!SSUc z*+ze8ICA_FwmK*GGAatIvKnnxlM?t6t3{m8^nV-k=GunuPMAYi#lbZ&Z{(3JOw!p3 zbbqj7e8UU21OxRUUQ*D3fx410j-&_R(NmxqH%ky1>Q|4(tct#zQKnzUEOS|*6Xsb% zNd^9)R5H3UOafeUzw9q!#i8gZ9GsbBWcd8MS_uh=9Opy+^XJ&0*`0=_CP0^V!QtXN=h#aSxDx))0;Nqf4!R{VwzDZc}; zhNs{DPx(Cu`1iC$Ht}p4bPikx$f=J{u1Mdfc|RQ5T4P&}XXK^I#|=bNt$#CIiq*n_ zF>um#uu+~;Bbn2!3TANOoJ@}?@34+Sj*$xLI8baQrVtth#JEMncGbr|AhqTcT1zHG z&i1cCvQ~{*X&n1eP7unYPy6V+?=cTBbU?0P%q<{ZzCaTgtZc)mH=DIYCBfkcnaSq z-vQq4GStA@INmq68KW%)F#~6v@kPupIb=&G)-Lv=R3Z@0ukmROjS^x36{1~P zE9nHMwe5U4-jDD#Nv@Hv4$lBn<**3z!Fd=sj>)(5xz@z9lABTNX)y6U(|^3IrH^;2 z59j%R{U}<0{KjZ?y)69$2Nitvy86|_tZ85qFA{9z;ETd&mSl{@1vkC#PcG>mA<~mq zbQU?Uf+5S<2_F)(-~@ZG1p|WFB@JpNj!dc!+MbGd)t@vJ_ZhlV`v}>#k#EOoGqaH8 z0Cbd9z9=O~gE}zjeA;;Y62&RV8u<~oURlqpKA<7MWF_&rw?KY^@r`y|P+t6wt9q7A zhJKZzI3i#Md>`98v`@?yhu_hk=T1~CVpHZgQgU?BJEGp;!{sD=dolwfhe=UbtIgis zjPimB|NQ=7_%j=14S>*`X@~ax)D6EC^GAqRLhc$HP_x*SHcqY89SN^I?-O@)xAXOZ zPW{$yO+HJ7q>c5x4d?~bar>%#ke>?6f9&E#U`(Pb0n zcY&V>iVs_?UU}kPaqBX0s`lr!z{90Z5EVJdP1jJU&W~~)+#;m!_gPAa(23{qiKvK% za*;O;^4-!kX3guItW z5}?vRymR;KJyYcJ_*GYV#Nsu1FaPmbl71Q7beROmh-8BlWwE90Hb}sNUxsGVjX5K} zsn_QCLFaX zpH*h^qxP2%mWM2B3EXB~$8ZZ+u%T2+sEr@BfCYYu%%hv8M!c?MX}{aKsb!#noopfG zt2xs;tW7!X-kf{C7F#S^ERg-`*%({+q)8g6i`}}SrM;-m%72%{1is{;T+t^qJYn zatZCQHfD)djv|rY2DTSuwHck`-C1cj zZ?BGidg4E+h*mk8Yz4RFVexMBSV-Cor^MacEFhw-MZm`NV?@4(aYmlvY_CheDp4Ne ze&B>W`K22zqI0=f_fsb?ywdezvnjb+-nSd>kNJ<^^L_?S4}+E{5gEa=W3qIE+oh66 znQBy>P)eK>QV-1zT6Nb+T*8(W>_bby=J!~&*lis6EH_ioL*r69{29uiao*2D3k+NX zF_qPgwxclW6?Gnuym+gVUY&6(;k0=(T`|p^l=wa}a?T>qY#N~1AKjlb7*WOJ0AxDB zn|+?U9DOn?C7HCJH_eUV`G4Ti^;+P@3i8i++Y<3eKSwx+`Hi2uA1zN|%-92P3>L-t zC)Y8h=B$`i;6B+_{uurM>q0E>cACI=u`bU;Sa2#k$lhw0LIxXt;xtA{!3=|>b>S#2 zGb#sYDVr>|4)T^|7jerFi$k1fC|s5~c7|GKj0_#rs1c=>#1JLiCtb?~@N%q`>81Yp z0&NHF;X4fvD@qDO;#?-YG9+dqN<>nG`lfE}#x)Y(>KB;ESW7rh@u4o2x{)I&z`dHE z-gB5y;oRO;txER|7}kohmSQ3J(p3$$(c68!IugjQrMdCIk>n*x^Zde%aSis zD^VFRRb+y^gAnL17?(pQKEgHn%Ta$69mv^k@7B;NokChTKCNP*$SN0L1>e}JQ}%3Tm?VD2w z$s4_}PVCrB32MYfoTq{ls*kUnH)TY^+ozG&zBG0B{Md}jC8NquUouAG(p^Tk8iRWk zy~{P}q=byvJAc80*UW}6tRj#+J4`WhOQZBH_Dav$;?c_9h6k_*W@|TO+X`1zEJ}Wp zfrJN;A{5}UVnfG#l)yB&K0{xA3o7R*a^t62E@9-R$|po9uwN~W%rxFDA;5~KKd75z zX@X{h$x|qat2s>OM#S>a;c!O4(}C)D1EEO?388n)Wb4{6xKQi3nTZhd-HJHAH|Jt; z@3mNI5j5PX4*U@XwCKKY*R5+e%m~O^Nd+Jupdh2Ly8Y&828rtzptp(IqMoB4Ek3>p z$X+U28ii>7Bu{pjz$L~~%We;$*RS4~koy(z3j_bX*1CEmNkOiZ_y|hX$kXfRD0QK) zzp_`%GKtVjE#n}(LHrhEjW0NnF4adP1NN&6n~6llONQxDh0BVxT#({7l;R&RsVMyE z3AiNm(X=GwgHPfEex%{o5T>4bKd{K6@{$XcpaZ)A9KH~B=?RcBMG3VlAvGZ~a2BGY zsbuLd{!Lx;Eg-)A*T#QQoIPHNTVNaIWms3!uot1Gc)6#E|KcCJ%T+IMhzM8#YNRtQ zxcI^MGy*<-!53g|Dj(%}=*P!2*&R~2>>Z@ziqb*&;>qV$sDZrm`^|H*J)BZg1a?f^ z4N=Lay6hcF|7ehhuGu&8X2T*jmaSzV%jRcSl^_`{a`>xf!su=iK;tylySM;zraOSJ<#UF{w?}S zXE?#Au@;s5nXCVc>;3s8a>i$bebj)O1>UsULQ+f>BX@){;U!Oz(7+HK(>-B?wHVf%W(NaQ`4B>Tt?-Tt<=}`;wOSWsc zw8=qzvKpl*2~ifv+q?eZG}*AkSHD0#vq17-iz z4CZ}7sQD98t{6Sw`jv_49j0D_3@Nxi%W`S-RAn?I!N$o0N^Fr)T$( z225?Axzx!E6?-HN)P%tQQ|1vv8FpR1Tv8`0&!RYF@dV!^kxMyCBBvnR88-{-4#((~ z0K_x;=ox8f{beKiaVlB=H_Ptb(?wf*jp_JU#>VCNN7C&Pv-<2DGc>s-e@9_(y&4up zk})*Do#8rE4d`Zz&LX%RFp%r_N{GI*zbv_a>0J(h=|j%UCNkg*O4y~*zh+=weeA*0 zQ4`cQ5?fxSyi8l6gx;d*WPNAGq-TxepJ9t(kW8ye=9(pb@xz3ic|N#GzFVdy<;<7EDoeT{N2&A1szAPuD-D zJ^$3uv_=;*ZqJG?unAupLK-c91w~u6a4U|tOqnZ*yBzaA8UY<@6dhRb!hmK-0uR_hSbpGKe1+*n(7!&WI?YfV**Y{d5ve7^1T<-Pst-SvyMEU2t zMSnn^`2-GmqU>9)bI{!M00ei=tUI1gT}WFI%JKg8YC_y0e~9aO7ynTkKbph)c;cvU zA%q4h%RaB!dFc55=V>IwffKe+$#;+j;XQHf6)o^oHtY?fJb&Iua$-TDU>Kyf9XIo% zrRJJ;Tp1XSThag8FUn6JWd8RI(4lq^5s<4wEYv)b)N_yj{7)^wd7nja2_%?u@jTzv z@pi1zP%ba=mUX<5TmwJpSEKaWYh8?kyjzw6d2Zk+M=+yn+&oCQFmHiI0dmoyxB6RO+(Bb^3@Ke zK|cfPS)jv@`)F2t&5S9&(k_c$kCNOK+AT<7V`}=(&Gl{0cok1IANFHgYWS+lc`QPS z1VD9eo#2#YsWOvTxQJB1P|kK~$8&n=B#nN0D&~-2>Ui|Yz)6VtKPtOE9LuymI6sz? zE^6^@$G#1qJj+-C20=~-jn$~yTfCpTxMr5@@-ajg_|t!kYdth_Lp)H{vJ?)Hb+G6{ zR@C7=27YsUN!EsC?1`3LJiw}q8AOm4Mtxq!W5{*PXiVaLUe zF#!YoPCdSGa;TLN|F=>}NbrN8KmDPfHpF=5&Zza1Wy=0P`k$xeH$|8knT@l1nRstX< z8ADr?31Ye9c*3uroR1}D=X$M*z73o5`1Up^;swS4VBUVgW}|B@P^-#Ge(8DXT=L+u zd_|1x7Y6q6OYBch4JVuLd0@E-U6gI<3ITq-fD~MWe;|y)ntr7G9v%IHcGGew2E}W3 zj`2hkLOH-6*M6^IFJQygrg&N?oSY6S?PUV z5b^$1$a*Md>%3*^Q7{N1-}C*RLiz(x@8-b&a3>GINA36O^S$;jlJNx;&_AU8v$u`* znirZ`Z~GdwFaJJCUI$XfMgX>psdM-D9XMk9Dq~Es&Fj?Gjl(EAZCePv%$Py&u5|op zTdRI*5yMC_v8!7=%Oj^z;q{qte^xAXOdX)aNY){VJHKH**YyY2 z1W^Qc5|QHOsyd++!77y+`1~YUp6|=mFYR|XrE2^br!!K>f&-#cnK*;*zzZX%gzg~w z9cIObX7V?@T@FJzVE`u?_z4w)>@gIO%W`7)u(G)ytO!0(r4`CW#^^1Q{)04d$Uh}F#D8(xS{bT(+w=4Db$(pGuL^A2GEXEr=h z(QUBvO4)7_B6QthmmV9acGE?H{@nDV9?NNS5Lc+fj$Q$J7y1v53of!QUG%vq z5RR3|2u0hFwy*Ma^w<%JX4R7Mzn?D#EoaZZNwZ$!ABvDD$;to;C%E-c8%T2gF9N7x z!7t>6VEX4v+V512DTaF;bslH@%kFtmUOqaWKd7Wpnc#%OvWqg;>Q?@3ZUJGg*8?}P_;t0f*t&ldwHT>JUjFl} zgaLUqUTGa2U>>HCM?Q+p9bN12RoBu7Du<`RIcq}=mL;?eK-RlHK#H}l+Gvixf{?oi~1&?e3X27Dk?lKMeTqR+20En2S?0di3`F<<@J zng*)0BOe=ww3VCO$vzry%U0|B30!t3aMJc}^wCJ9@-**)#3%ta>jsZB;C_pY-yynx zhQ2QdA;g%#fer+vrgj!8K-#GKrKBtZL>w6z_Ua;b+!+SoiYy#qAp5g%xqC3vep}57ssDAx#72z5|6*R ztz+A|$ZDN(`yb8R1OJF+^>tgKh?`U-;Xi0z=~z3?G>;iC4Nd)6&>-<5U33Ii$ix?20bsF)}E zUjx-mrEGHa3c#1NEBLRD_8#vi*uWmi8mRC28CY$K-GuvFfLOgMU;f#j7m;;xS5B=T zP&gR{_`Zh2>5{x^#>jJDZlO(YqCFl5_OhQz^z8-&$b%^X?YyVBabhDGnU7w zC!M_&id)vXGfh1c2jUQ4U`)tD?~&T|{h0P$G|HVEB-1-~=1tWT#^y)=p0{DyfVrmE z=F;iqs7wG1D=ELo*hkzr@U^d#WX?QFo2Rj6T{1r|Vl$HqGzEKgb7k-mV^D>e>F$fG z{)8~BXX1jHk4oNvdXz}{BeK%AnrNFUlF3=ONOK2q0XZ7=$w!XT#c%lQw(HM_jQ||C@MJ0p=Iv%59FS z=Yx?@gF=8I{SyL({A}$G9!>VZ_%_5_0gcK{Ne%}+iI{u3{ls&FfB8@+P{Fu)fhw)E z5aIW!5&waP39n0e0hOClEOX`~=$CiL?Za3quH26z;8l_5t) zCW|~pk8f2^Ehq^^2EjHwAaUe?PsAL%EZ!HDTTuzYqiAXKem9K+@!-=q&;S=V=U@09 zEZ>>_FqZgR{{s{H_C1Jh-XRLQdl5#Wy8QC!|A~vG3vgT$ata_xm*<;KYUB|Apae;H zT0%2Oaf$_&J2d;^uYh_9{J%wZ!lNr6$=(7HE9{TSc9*XG$e1<+9{mx1CHqg1)j3z| z>y=KqyFes*g^aBRkX>839ZYoVnq*`DB&(Ja-M4dRY@fLP*CZ7VE1(q<{f>sS=FE|c zMNu2^?@&q~xD=-$odx0~ryGCL+4PSz@%o|^pe}k*g{TY2$?2Q4DkLALRwXyd-sB4d z3!dp$aZU*=)H9k=$J8m1jmiZr6P=1g7cXuXCY90ue>C513`hI+7X5rDR@sHC@o9OD`AUc3BEj*Ef! zMNq~=3SJ#o3c|05&MYB$kFdp2xr!#ctjOlmaiZN%m=Goh5qP1VmpcsachUt(oq6$u zNR7zJnVaVF^$u)}>|?!^z_4%~%U}kc4;3?4zba>M1Ir&?ys`x9mUZ(Ko*(Mk4S`qH z*j~tT+p*5~N*)D$bfqi^(+~JhvfAHEw;9QP4Ww0fzU28pUdX+O1*oSL>si!-N%_Ch z6cuw5oRQMDJ|y~~Rfkp-(dm15taLw4zP1P~RppkeV4&&<_cuWhL~9TAxc81rPd|I} z)w0){J*I&}mbU)~et6sl;jEF+VZz2#ue0y0Ii^m6>_Mu(Fqp~v%ZA$><68kt1s@bQ zWn3=I6o(4Cxf>Qt$m+KIa08}i^ZdA?h6O7)@{6R-Dkdj~n_W!33!%f&AiLROcB!D+|2_J0OBCf^x(~thYGpP%>UhH zM71C*EyLGvJX*96Ew>h<>FJ!@S70u6Pkj)&zDbOrFO=5EDHQ~n5~AHOtRl-1$#kLT zl*}3nMAQ7l|HNXNkdU@R31s?>T+$y@z?gehoUViJn(KH)cD1Sp*E*!2t};$#D(Ks{ zACHJ!W0P<-v;#j>UYTwj;Jl;SO)4Pvo%8Gu zf}TSKHoUZ175(wO{ih&L;n7=wo?mVD$Y5;s9O&=7{+4s?BnogvlVZ4&o#7*+Hi$cm zA+R0(=UY4_0>x;+DM!I99O7#47?taOt2EtF-k&ze$JeFJrG5wG6B{w`$kh0izF`u8 zPTcz8ogqv&%y0gyQsXIGbiiHofeItRSk3fhWnm(TUo_2Pz%qofn$Eq^o^H&Qcwh>( zAEUKs__x&dNxVCTGriV_Mo6QFQAA@sMfN#4KI@J`i0OWcU!nS7jAc&5$ftiZRH8wD zJCOZ?KM8}0|96C7GlQ7Ma=t1v2B@%dd$*cK!vl7Io} zhi=)1W`QKB;*9B<%Pmz28t<((-nMP!B$gQ^35+p#p?6&P^% zfxzK8rM0pH-<=0r3WN&tAhsg3$H2lbn+Zg%{pSTTt0q@~7eEKp&XF-M9;@tXmM+gK zdr=|*p(F%gF5$E#CkVXraPLhd1R>Q4baM2PC#&AQiy7lS zXOz@73OqU1xmSj#%tL_SH!Qd8cMTK1%Sj4w2F~;!hko4A9)V8>Q64kzVYT3Yr1&Vz z`~3#fc!Cso#yAle(sqH!a<;4Ok)96hoQc)^SGboys_gv^-vKrVx =$=!B0SS$b+a3U70`f#6OK3VZYEuPJM8V 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

-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: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 PkNoLq>T8s#*#`Z8x^&=c diff --git a/docker-compose.images.yml b/docker-compose.images.yml index 8909aae794..c94107653e 100644 --- a/docker-compose.images.yml +++ b/docker-compose.images.yml @@ -1,8 +1,5 @@ -# for running in docker compose with prebuilt images - -# version is now using "compose spec" -# v2 and v3 are now combined! -# docker-compose v1.27+ required +# Ironhack note - This file is for running in docker compose with prebuilt images +# Use these if you're stuck, or if you want to start slowly migrating from external images into your own. services: vote: @@ -39,11 +36,6 @@ services: redis: image: redis:alpine - volumes: - - "./healthchecks:/healthchecks" - healthcheck: - test: /healthchecks/redis.sh - interval: "5s" networks: - back-tier @@ -54,10 +46,6 @@ services: POSTGRES_PASSWORD: "postgres" volumes: - "db-data:/var/lib/postgresql/data" - - "./healthchecks:/healthchecks" - healthcheck: - test: /healthchecks/postgres.sh - interval: "5s" networks: - back-tier diff --git a/docker-compose.yml b/docker-compose.yml index 5915ffd741..7da94f4e2d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,3 @@ -# version is now using "compose spec" -# v2 and v3 are now combined! -# docker-compose v1.27+ required - services: vote: build: diff --git a/docker-stack.yml b/docker-stack.yml deleted file mode 100644 index 356b944caf..0000000000 --- a/docker-stack.yml +++ /dev/null @@ -1,53 +0,0 @@ -# this file is meant for Docker Swarm stacks only -# trying it in compose will fail because of multiple replicas trying to bind to the same port -# Swarm currently does not support Compose Spec, so we'll pin to the older version 3.9 - -version: "3.9" - -services: - - redis: - image: redis:alpine - networks: - - frontend - - db: - image: postgres:15-alpine - environment: - POSTGRES_USER: "postgres" - POSTGRES_PASSWORD: "postgres" - volumes: - - db-data:/var/lib/postgresql/data - networks: - - backend - - vote: - image: dockersamples/examplevotingapp_vote - ports: - - 8080:80 - networks: - - frontend - deploy: - replicas: 2 - - result: - image: dockersamples/examplevotingapp_result - ports: - - 8081:80 - networks: - - backend - - worker: - image: dockersamples/examplevotingapp_worker - networks: - - frontend - - backend - deploy: - replicas: 2 - -networks: - frontend: - backend: - -volumes: - db-data: diff --git a/healthchecks/postgres.sh b/healthchecks/postgres.sh deleted file mode 100755 index 299416740e..0000000000 --- a/healthchecks/postgres.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -set -eo pipefail - -host="$(hostname -i || echo '127.0.0.1')" -user="${POSTGRES_USER:-postgres}" -db="${POSTGRES_DB:-$POSTGRES_USER}" -export PGPASSWORD="${POSTGRES_PASSWORD:-}" - -args=( - # force postgres to not use the local unix socket (test "external" connectibility) - --host "$host" - --username "$user" - --dbname "$db" - --quiet --no-align --tuples-only -) - -if select="$(echo 'SELECT 1' | psql "${args[@]}")" && [ "$select" = '1' ]; then - exit 0 -fi - -exit 1 diff --git a/healthchecks/redis.sh b/healthchecks/redis.sh deleted file mode 100755 index 3953758f94..0000000000 --- a/healthchecks/redis.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/sh -set -eo pipefail - -host="$(hostname -i || echo '127.0.0.1')" - -if ping="$(redis-cli -h "$host" ping)" && [ "$ping" = 'PONG' ]; then - exit 0 -fi - -exit 1 diff --git a/k8s-specifications/db-deployment.yaml b/k8s-specifications/db-deployment.yaml deleted file mode 100644 index bc94ca7368..0000000000 --- a/k8s-specifications/db-deployment.yaml +++ /dev/null @@ -1,33 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: db - name: db -spec: - replicas: 1 - selector: - matchLabels: - app: db - template: - metadata: - labels: - app: db - spec: - containers: - - image: postgres:15-alpine - name: postgres - env: - - name: POSTGRES_USER - value: postgres - - name: POSTGRES_PASSWORD - value: postgres - ports: - - containerPort: 5432 - name: postgres - volumeMounts: - - mountPath: /var/lib/postgresql/data - name: db-data - volumes: - - name: db-data - emptyDir: {} diff --git a/k8s-specifications/db-service.yaml b/k8s-specifications/db-service.yaml deleted file mode 100644 index 104f1e8268..0000000000 --- a/k8s-specifications/db-service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: db - name: db -spec: - type: ClusterIP - ports: - - name: "db-service" - port: 5432 - targetPort: 5432 - selector: - app: db - diff --git a/k8s-specifications/redis-deployment.yaml b/k8s-specifications/redis-deployment.yaml deleted file mode 100644 index 24aa52135f..0000000000 --- a/k8s-specifications/redis-deployment.yaml +++ /dev/null @@ -1,28 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: redis - name: redis -spec: - replicas: 1 - selector: - matchLabels: - app: redis - template: - metadata: - labels: - app: redis - spec: - containers: - - image: redis:alpine - name: redis - ports: - - containerPort: 6379 - name: redis - volumeMounts: - - mountPath: /data - name: redis-data - volumes: - - name: redis-data - emptyDir: {} diff --git a/k8s-specifications/redis-service.yaml b/k8s-specifications/redis-service.yaml deleted file mode 100644 index 809d31d875..0000000000 --- a/k8s-specifications/redis-service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: redis - name: redis -spec: - type: ClusterIP - ports: - - name: "redis-service" - port: 6379 - targetPort: 6379 - selector: - app: redis - diff --git a/k8s-specifications/result-deployment.yaml b/k8s-specifications/result-deployment.yaml deleted file mode 100644 index b85488a667..0000000000 --- a/k8s-specifications/result-deployment.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: result - name: result -spec: - replicas: 1 - selector: - matchLabels: - app: result - template: - metadata: - labels: - app: result - spec: - containers: - - image: dockersamples/examplevotingapp_result - name: result - ports: - - containerPort: 80 - name: result diff --git a/k8s-specifications/result-service.yaml b/k8s-specifications/result-service.yaml deleted file mode 100644 index 0fed5e0cc5..0000000000 --- a/k8s-specifications/result-service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: result - name: result -spec: - type: NodePort - ports: - - name: "result-service" - port: 8081 - targetPort: 80 - nodePort: 31001 - selector: - app: result diff --git a/k8s-specifications/vote-deployment.yaml b/k8s-specifications/vote-deployment.yaml deleted file mode 100644 index 165a9478f8..0000000000 --- a/k8s-specifications/vote-deployment.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: vote - name: vote -spec: - replicas: 1 - selector: - matchLabels: - app: vote - template: - metadata: - labels: - app: vote - spec: - containers: - - image: dockersamples/examplevotingapp_vote - name: vote - ports: - - containerPort: 80 - name: vote diff --git a/k8s-specifications/vote-service.yaml b/k8s-specifications/vote-service.yaml deleted file mode 100644 index d7a05b5513..0000000000 --- a/k8s-specifications/vote-service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - labels: - app: vote - name: vote -spec: - type: NodePort - ports: - - name: "vote-service" - port: 8080 - targetPort: 80 - nodePort: 31000 - selector: - app: vote - diff --git a/k8s-specifications/worker-deployment.yaml b/k8s-specifications/worker-deployment.yaml deleted file mode 100644 index 9e35450aec..0000000000 --- a/k8s-specifications/worker-deployment.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - labels: - app: worker - name: worker -spec: - replicas: 1 - selector: - matchLabels: - app: worker - template: - metadata: - labels: - app: worker - spec: - containers: - - image: dockersamples/examplevotingapp_worker - name: worker diff --git a/result/docker-compose.test.yml b/result/docker-compose.test.yml deleted file mode 100644 index 57ddc55d87..0000000000 --- a/result/docker-compose.test.yml +++ /dev/null @@ -1,62 +0,0 @@ -version: '2' - -services: - - sut: - build: ./tests/ - depends_on: - - vote - - result - - worker - networks: - - front-tier - - vote: - build: ../vote/ - ports: ["80"] - depends_on: - - redis - - db - networks: - - front-tier - - back-tier - - result: - build: . - ports: ["80"] - depends_on: - - redis - - db - networks: - - front-tier - - back-tier - - worker: - build: ../worker/ - depends_on: - - redis - - db - networks: - - back-tier - - redis: - image: redis:alpine - networks: - - back-tier - - db: - image: postgres:9.4 - environment: - POSTGRES_USER: "postgres" - POSTGRES_PASSWORD: "postgres" - volumes: - - "db-data:/var/lib/postgresql/data" - networks: - - back-tier - -volumes: - db-data: - -networks: - front-tier: - back-tier: diff --git a/result/tests/Dockerfile b/result/tests/Dockerfile deleted file mode 100644 index b8b6e90520..0000000000 --- a/result/tests/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM node:8.9-slim - -RUN apt-get update -qq && apt-get install -qy \ - ca-certificates \ - bzip2 \ - curl \ - libfontconfig \ - --no-install-recommends -RUN yarn global add phantomjs-prebuilt -ADD . /app -WORKDIR /app -CMD ["/app/tests.sh"] diff --git a/result/tests/render.js b/result/tests/render.js deleted file mode 100644 index 975137bf3b..0000000000 --- a/result/tests/render.js +++ /dev/null @@ -1,15 +0,0 @@ -var system = require('system'); -var page = require('webpage').create(); -var url = system.args[1]; - -page.onLoadFinished = function() { - setTimeout(function(){ - console.log(page.content); - phantom.exit(); - }, 1000); -}; - -page.open(url, function() { - page.evaluate(function() { - }); -}); diff --git a/result/tests/tests.sh b/result/tests/tests.sh deleted file mode 100755 index 448159454b..0000000000 --- a/result/tests/tests.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -while ! timeout 1 bash -c "echo > /dev/tcp/vote/80"; do - sleep 1 -done - -curl -sS -X POST --data "vote=b" http://vote > /dev/null -sleep 10 - -if phantomjs render.js http://result | grep -q '1 vote'; then - echo -e "\\e[42m------------" - echo -e "\\e[92mTests passed" - echo -e "\\e[42m------------" - exit 0 -else - echo -e "\\e[41m------------" - echo -e "\\e[91mTests failed" - echo -e "\\e[41m------------" - exit 1 -fi diff --git a/seed-data/Dockerfile b/seed-data/Dockerfile deleted file mode 100644 index f970e42ad4..0000000000 --- a/seed-data/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM python:3.9-slim - -# add apache bench (ab) tool -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - apache2-utils \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /seed - -COPY . . - -# create POST data files with ab friendly formats -RUN python make-data.py - -CMD ["/seed/generate-votes.sh"] diff --git a/seed-data/generate-votes.sh b/seed-data/generate-votes.sh deleted file mode 100755 index 2f82563ccd..0000000000 --- a/seed-data/generate-votes.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -# create 3000 votes (2000 for option a, 1000 for option b) -ab -n 1000 -c 50 -p posta -T "application/x-www-form-urlencoded" http://vote/ -ab -n 1000 -c 50 -p postb -T "application/x-www-form-urlencoded" http://vote/ -ab -n 1000 -c 50 -p posta -T "application/x-www-form-urlencoded" http://vote/ diff --git a/seed-data/make-data.py b/seed-data/make-data.py deleted file mode 100644 index def9f9755d..0000000000 --- a/seed-data/make-data.py +++ /dev/null @@ -1,13 +0,0 @@ -# this creates urlencode-friendly files without EOL -import urllib.parse - -outfile = open('postb', 'w') -params = ({ 'vote': 'b' }) -encoded = urllib.parse.urlencode(params) -outfile.write(encoded) -outfile.close() -outfile = open('posta', 'w') -params = ({ 'vote': 'a' }) -encoded = urllib.parse.urlencode(params) -outfile.write(encoded) -outfile.close() From a00e9fd2bfc6a0866ac26e30fce7bb280baa0a6f Mon Sep 17 00:00:00 2001 From: Diogo Barros Date: Mon, 9 Dec 2024 13:25:32 +0000 Subject: [PATCH 02/21] readding healthchecks --- docker-compose.images.yml | 9 +++++++++ healthchecks/postgres.sh | 21 +++++++++++++++++++++ healthchecks/redis.sh | 10 ++++++++++ 3 files changed, 40 insertions(+) create mode 100755 healthchecks/postgres.sh create mode 100755 healthchecks/redis.sh diff --git a/docker-compose.images.yml b/docker-compose.images.yml index c94107653e..a508c772c2 100644 --- a/docker-compose.images.yml +++ b/docker-compose.images.yml @@ -36,6 +36,11 @@ services: redis: image: redis:alpine + volumes: + - "./healthchecks:/healthchecks" + healthcheck: + test: /healthchecks/redis.sh + interval: "5s" networks: - back-tier @@ -46,6 +51,10 @@ services: POSTGRES_PASSWORD: "postgres" volumes: - "db-data:/var/lib/postgresql/data" + - "./healthchecks:/healthchecks" + healthcheck: + test: /healthchecks/postgres.sh + interval: "5s" networks: - back-tier diff --git a/healthchecks/postgres.sh b/healthchecks/postgres.sh new file mode 100755 index 0000000000..299416740e --- /dev/null +++ b/healthchecks/postgres.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -eo pipefail + +host="$(hostname -i || echo '127.0.0.1')" +user="${POSTGRES_USER:-postgres}" +db="${POSTGRES_DB:-$POSTGRES_USER}" +export PGPASSWORD="${POSTGRES_PASSWORD:-}" + +args=( + # force postgres to not use the local unix socket (test "external" connectibility) + --host "$host" + --username "$user" + --dbname "$db" + --quiet --no-align --tuples-only +) + +if select="$(echo 'SELECT 1' | psql "${args[@]}")" && [ "$select" = '1' ]; then + exit 0 +fi + +exit 1 diff --git a/healthchecks/redis.sh b/healthchecks/redis.sh new file mode 100755 index 0000000000..3953758f94 --- /dev/null +++ b/healthchecks/redis.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -eo pipefail + +host="$(hostname -i || echo '127.0.0.1')" + +if ping="$(redis-cli -h "$host" ping)" && [ "$ping" = 'PONG' ]; then + exit 0 +fi + +exit 1 From 4e2b397bd8ab8db67507212828d03cb301c7ebed Mon Sep 17 00:00:00 2001 From: Diogo Barros Date: Fri, 13 Dec 2024 10:08:03 +0000 Subject: [PATCH 03/21] first row of fixes (dockerfiles and docker-compose) --- worker/.dockerignore | 7 +++++++ worker/Dockerfile | 31 ++++++++++--------------------- worker/Worker.csproj | 4 ++-- 3 files changed, 19 insertions(+), 23 deletions(-) create mode 100644 worker/.dockerignore diff --git a/worker/.dockerignore b/worker/.dockerignore new file mode 100644 index 0000000000..295aa4ff92 --- /dev/null +++ b/worker/.dockerignore @@ -0,0 +1,7 @@ +# Exclude build artifacts and unnecessary files +bin/ +obj/ +*.md +*.git +*.gitignore +.vscode/ diff --git a/worker/Dockerfile b/worker/Dockerfile index a3f92d7e94..118aebf072 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -1,27 +1,16 @@ -# because of dotnet, we always build on amd64, and target platforms in cli -# dotnet doesn't support QEMU for building or running. -# (errors common in arm/v7 32bit) https://github.com/dotnet/dotnet-docker/issues/1537 -# https://hub.docker.com/_/microsoft-dotnet -# hadolint ignore=DL3029 -# to build for a different platform than your host, use --platform= -# for example, if you were on Intel (amd64) and wanted to build for ARM, you would use: -# docker buildx build --platform "linux/arm64/v8" . - -# build compiles the program for the builder's local platform -FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:7.0 AS build -ARG TARGETPLATFORM -ARG TARGETARCH -ARG BUILDPLATFORM -RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM" - +# Stage 1: Build +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build WORKDIR /source -COPY *.csproj . -RUN dotnet restore -a $TARGETARCH -COPY . . -RUN dotnet publish -c release -o /app -a $TARGETARCH --self-contained false --no-restore +# Copy the project file and restore dependencies +COPY *.csproj ./ +RUN dotnet restore + +# Copy the remaining source code and publish +COPY . ./ +RUN dotnet publish -c Release -o /app --runtime linux-arm64 --self-contained false --no-restore -# app image +# Stage 2: Runtime FROM mcr.microsoft.com/dotnet/runtime:7.0 WORKDIR /app COPY --from=build /app . diff --git a/worker/Worker.csproj b/worker/Worker.csproj index 00845078ef..cd843f8247 100644 --- a/worker/Worker.csproj +++ b/worker/Worker.csproj @@ -1,8 +1,8 @@ - Exe net7.0 + linux-arm64 @@ -11,4 +11,4 @@ - \ No newline at end of file + From 89b9590f92616659185b329eec6fc507d2c9a4d3 Mon Sep 17 00:00:00 2001 From: InstructorDiogo Date: Tue, 17 Dec 2024 11:18:46 +0000 Subject: [PATCH 04/21] fixing docker compose --- docker-compose.yml | 17 +---------------- example-voting-app.sln | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 example-voting-app.sln diff --git a/docker-compose.yml b/docker-compose.yml index 7da94f4e2d..070e370843 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,6 @@ services: result: build: ./result - # use nodemon rather than node for local dev entrypoint: nodemon --inspect=0.0.0.0 server.js depends_on: db: @@ -37,8 +36,7 @@ services: - back-tier worker: - build: - context: ./worker + image: dockersamples/examplevotingapp_worker depends_on: redis: condition: service_healthy @@ -71,19 +69,6 @@ services: networks: - back-tier - # this service runs once to seed the database with votes - # it won't run unless you specify the "seed" profile - # docker compose --profile seed up -d - seed: - build: ./seed-data - profiles: ["seed"] - depends_on: - vote: - condition: service_healthy - networks: - - front-tier - restart: "no" - volumes: db-data: diff --git a/example-voting-app.sln b/example-voting-app.sln new file mode 100644 index 0000000000..cdaa30a460 --- /dev/null +++ b/example-voting-app.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Worker", "worker\Worker.csproj", "{94048AEC-7282-41A6-8B5B-5FAAF86FD92A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {94048AEC-7282-41A6-8B5B-5FAAF86FD92A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {94048AEC-7282-41A6-8B5B-5FAAF86FD92A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {94048AEC-7282-41A6-8B5B-5FAAF86FD92A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {94048AEC-7282-41A6-8B5B-5FAAF86FD92A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {43E7D34E-C638-4020-A69D-32DF75729BAB} + EndGlobalSection +EndGlobal From 3ff62b67c94707ec65f207dd8fc64a614653a41d Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Wed, 18 Dec 2024 15:19:49 +0100 Subject: [PATCH 05/21] Adding Terraform configurations + started Ansible configurations --- .gitignore | 6 ++ ansible/ansible.cfg | 2 + ansible/playbook.yml | 57 ++++++++++++ docker-compose.images.yml | 4 +- terraform/alb.tf | 161 ++++++++++++++++++++++++++++++++++ terraform/internet-gateway.tf | 6 ++ terraform/main.tf | 146 ++++++++++++++++++++++++++++++ terraform/nat-gw.tf | 16 ++++ terraform/outputs.tf | 43 +++++++++ terraform/routes.tf | 38 ++++++++ terraform/terraform.tfvars | 14 +++ terraform/variables.tf | 65 ++++++++++++++ 12 files changed, 556 insertions(+), 2 deletions(-) create mode 100644 ansible/ansible.cfg create mode 100644 ansible/playbook.yml create mode 100644 terraform/alb.tf create mode 100644 terraform/internet-gateway.tf create mode 100644 terraform/main.tf create mode 100644 terraform/nat-gw.tf create mode 100644 terraform/outputs.tf create mode 100644 terraform/routes.tf create mode 100644 terraform/terraform.tfvars create mode 100644 terraform/variables.tf diff --git a/.gitignore b/.gitignore index 9a6f694990..cc8b90586d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,9 @@ bin/ obj/ .vs/ node_modules/ +.terraform.lock.hcl +terraform.tfstate +terraform.tfstate.backup +.terraform/ +dump.rdb +voting-app-key.pem diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..e055ae038c --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,2 @@ +[defaults] +inventory = hosts.ini \ No newline at end of file diff --git a/ansible/playbook.yml b/ansible/playbook.yml new file mode 100644 index 0000000000..0b7a3bf59c --- /dev/null +++ b/ansible/playbook.yml @@ -0,0 +1,57 @@ +##### Vote server ##### +- hosts: vote + become: true + tasks: + - name: Install Docker + apt: + name: docker.io + state: present + update_cache: yes + + - name: Pull Docker image for "vote" + docker_image: + name: ghenac/voting-app + source: pull + tag: vote + when: inventory_hostname == 'vote' + + - name: Run vote service + docker_container: + name: vote-service + image: ghenac/voting-app:vote + ports: + - "8080:8080" + state: started + when: inventory_hostname == 'vote' + +##### Result Server ##### + - name: Pull Docker image for "result" + docker_image: + name: ghenac/voting-app + source: pull + tag: result + when: inventory_hostname == 'result' + + - name: Run result service + docker_container: + name: result-service + image: ghenac/voting-app:result + ports: + - "8081:8081" + state: started + when: inventory_hostname == 'result' + +##### Worker Server ##### + - name: Pull Docker image for "worker" + docker_image: + name: dockersamples/examplevotingapp_worker + source: pull + tag: latest + when: inventory_hostname == 'worker' + + - name: Run worker service + docker_container: + name: worker-service + image: dockersamples/examplevotingapp_worker:latest + state: started + when: inventory_hostname == 'worker' \ No newline at end of file diff --git a/docker-compose.images.yml b/docker-compose.images.yml index a508c772c2..719783c62d 100644 --- a/docker-compose.images.yml +++ b/docker-compose.images.yml @@ -3,7 +3,7 @@ services: vote: - image: dockersamples/examplevotingapp_vote + image: ghenac/voting-app:vote depends_on: redis: condition: service_healthy @@ -14,7 +14,7 @@ services: - back-tier result: - image: dockersamples/examplevotingapp_result + image: ghenac/voting-app:result depends_on: db: condition: service_healthy diff --git a/terraform/alb.tf b/terraform/alb.tf new file mode 100644 index 0000000000..9055197127 --- /dev/null +++ b/terraform/alb.tf @@ -0,0 +1,161 @@ +##### Application Load Balancer ##### +resource "aws_lb" "voting_app_alb" { + name = "voting-app-alb" + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.public_security_group.id] + subnets = [ + aws_subnet.public_subnet.id, + aws_subnet.private_subnet.id + ] + + enable_deletion_protection = false + idle_timeout = 60 + drop_invalid_header_fields = true + + tags = { + Name = "voting-app-alb" + } +} + + +##### Target Groups ##### + +# Target group for 'vote' service # +resource "aws_lb_target_group" "vote_target_group" { + name = "vote-tg" + port = 80 + protocol = "HTTP" + vpc_id = aws_vpc.voting-app-vpc.id + target_type = "instance" + tags = { + Name = "vote-tg" + } +} + +# Target group for 'result' service # +resource "aws_lb_target_group" "result_target_group" { + name = "result-tg" + port = 80 + protocol = "HTTP" + vpc_id = aws_vpc.voting-app-vpc.id + target_type = "instance" + tags = { + Name = "result-tg" + } +} + +# Target group for 'worker' service # +resource "aws_lb_target_group" "worker_target_group" { + name = "worker-tg" + port = 80 + protocol = "HTTP" + vpc_id = aws_vpc.voting-app-vpc.id + target_type = "instance" + tags = { + Name = "worker-tg" + } +} + +##### Listeners ##### + +# HTTP Listener for ALB # +resource "aws_lb_listener" "voting_app_listener" { + load_balancer_arn = aws_lb.voting_app_alb.arn + port = 80 + protocol = "HTTP" + + default_action { + type = "fixed-response" + fixed_response { + content_type = "text/plain" + message_body = "This is the default action." + status_code = "404" + } + } +} + +##### Listener Rules ##### + +# Rule for 'vote' traffic # +resource "aws_lb_listener_rule" "vote_rule" { + listener_arn = aws_lb_listener.voting_app_listener.arn + priority = 1 + tags = { + Name = "vote" + } + + condition { + host_header { + values = ["vote.example.com"] + } + } + + action { + type = "forward" + target_group_arn = aws_lb_target_group.vote_target_group.arn + } +} + +# Rule for 'result' traffic # +resource "aws_lb_listener_rule" "result_rule" { + listener_arn = aws_lb_listener.voting_app_listener.arn + priority = 2 + tags = { + Name = "result" + } + + condition { + host_header { + values = ["result.example.com"] + } + } + + action { + type = "forward" + target_group_arn = aws_lb_target_group.result_target_group.arn + } +} + +# Rule for 'worker' traffic # +resource "aws_lb_listener_rule" "worker_rule" { + listener_arn = aws_lb_listener.voting_app_listener.arn + priority = 3 + tags = { + Name = "worker" + } + + condition { + host_header { + values = ["worker.example.com"] + } + } + + action { + type = "forward" + target_group_arn = aws_lb_target_group.worker_target_group.arn + } +} + +##### Attach Instances to Target Groups ##### + +# Register 'vote' instance # +resource "aws_lb_target_group_attachment" "vote_attachment" { + target_group_arn = aws_lb_target_group.vote_target_group.arn + target_id = aws_instance.vote.id + port = 80 +} + +# Register 'result' instance # +resource "aws_lb_target_group_attachment" "result_attachment" { + target_group_arn = aws_lb_target_group.result_target_group.arn + target_id = aws_instance.result.id + port = 80 +} + +# Register 'worker' instance # +resource "aws_lb_target_group_attachment" "worker_attachment" { + target_group_arn = aws_lb_target_group.worker_target_group.arn + target_id = aws_instance.worker.id + port = 80 +} diff --git a/terraform/internet-gateway.tf b/terraform/internet-gateway.tf new file mode 100644 index 0000000000..b166a56f16 --- /dev/null +++ b/terraform/internet-gateway.tf @@ -0,0 +1,6 @@ +resource "aws_internet_gateway" "gw" { + vpc_id = aws_vpc.voting-app-vpc.id + tags = { + Name = "internet-gw-voting-app" + } +} \ No newline at end of file diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000000..4f175c2132 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,146 @@ +##### Providers ##### +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.40.0" + } + } +} + +provider "aws" { + region = "us-east-1" +} + +##### VPC ##### +resource "aws_vpc" "voting-app-vpc" { + cidr_block = var.cidr_block + + tags = { + Name = var.vpc_name + } +} + +##### EC2 Instances ##### +resource "aws_instance" "vote" { + ami = var.ami_image + instance_type = var.instance_type + key_name = "voting-app-key" + availability_zone = "us-east-1a" + vpc_security_group_ids = [aws_security_group.public_security_group.id] + subnet_id = aws_subnet.public_subnet.id + associate_public_ip_address = true + tags = { + Name = "vote" + } +} + +resource "aws_instance" "result" { + ami = var.ami_image + instance_type = var.instance_type + key_name = "voting-app-key" + availability_zone = "us-east-1a" + vpc_security_group_ids = [aws_security_group.public_security_group.id] + subnet_id = aws_subnet.public_subnet.id + associate_public_ip_address = true + tags = { + Name = "result" + } +} + +resource "aws_instance" "worker" { + ami = var.ami_image + instance_type = var.instance_type + key_name = "voting-app-key" + availability_zone = "us-east-1a" + vpc_security_group_ids = [aws_security_group.public_security_group.id] + subnet_id = aws_subnet.public_subnet.id + associate_public_ip_address = true + tags = { + Name = "worker" + } +} + +##### Subnets ##### + +# Public # +resource "aws_subnet" "public_subnet" { + vpc_id = aws_vpc.voting-app-vpc.id + cidr_block = var.public_subnet_cidr_block + map_public_ip_on_launch = true + availability_zone = var.availability_zone["public_subnet_az"] + tags = { + Name = "public_subnet" + } +} + +# Private # +resource "aws_subnet" "private_subnet" { + vpc_id = aws_vpc.voting-app-vpc.id + map_public_ip_on_launch = false + availability_zone = var.availability_zone["private_subnet_az"] + cidr_block = var.private_subnet_cidr_block + tags = { + Name = "private_subnet" + } +} + +##### Security Groups ##### + +# Public # +resource "aws_security_group" "public_security_group" { + name = "voting-app_public-sec-gr" + description = "Allow public traffic" + vpc_id = aws_vpc.voting-app-vpc.id + + ingress { + description = "TLS from Internet" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "Http from Internet" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + tags = { + Name = "voting-app_public-sec-gr" + } +} + +# Private # +resource "aws_security_group" "private_security_group" { + name = "voting-app_private-sec-gr" + description = "Allow traffic on private VM" + vpc_id = aws_vpc.voting-app-vpc.id + + ingress { + description = "TLS from Internet" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [aws_subnet.public_subnet.cidr_block] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + tags = { + Name = "voting-app_private-sec-gr" + } +} \ No newline at end of file diff --git a/terraform/nat-gw.tf b/terraform/nat-gw.tf new file mode 100644 index 0000000000..87555a764e --- /dev/null +++ b/terraform/nat-gw.tf @@ -0,0 +1,16 @@ +resource "aws_nat_gateway" "nat_gw" { + allocation_id = aws_eip.nat_eip.id + subnet_id = aws_subnet.public_subnet.id + tags = { + Name = "nat-gw_voting-app" + } +} + +# Elastic IP for the NAT Gateway + +resource "aws_eip" "nat_eip" { + domain = "vpc" + tags = { + Name = "nat-gw_voting-app_eip" + } +} \ No newline at end of file diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000000..1fa3da4efe --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,43 @@ +##### Outputs for EC2 Instances ##### +output "vote_instance_public_ip" { + description = "The public IP address of the 'vote' EC2 instance" + value = aws_instance.vote.public_ip +} + +output "result_instance_public_ip" { + description = "The public IP address of the 'result' EC2 instance" + value = aws_instance.result.public_ip +} + +output "worker_instance_public_ip" { + description = "The public IP address of the 'worker' EC2 instance" + value = aws_instance.worker.public_ip +} + +##### Outputs for Subnets ##### +output "public_subnet_id" { + description = "The ID of the public subnet" + value = aws_subnet.public_subnet.id +} + +output "private_subnet_id" { + description = "The ID of the private subnet" + value = aws_subnet.private_subnet.id +} + +##### Outputs for VPC ##### +output "vpc_id" { + description = "The ID of the VPC" + value = aws_vpc.voting-app-vpc.id +} + +##### Outputs for Security Groups ##### +output "public_security_group_id" { + description = "The ID of the public security group" + value = aws_security_group.public_security_group.id +} + +output "private_security_group_id" { + description = "The ID of the private security group" + value = aws_security_group.private_security_group.id +} diff --git a/terraform/routes.tf b/terraform/routes.tf new file mode 100644 index 0000000000..ccc4903d5d --- /dev/null +++ b/terraform/routes.tf @@ -0,0 +1,38 @@ +##### Public Route ##### + +resource "aws_route_table" "public_route" { + vpc_id = aws_vpc.voting-app-vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.gw.id + } + tags = { + Name = "public_route" + } +} + +resource "aws_route_table_association" "public_association" { + subnet_id = aws_subnet.public_subnet.id + route_table_id = aws_route_table.public_route.id +} + + +##### Private Route ##### + +resource "aws_route_table" "private_route" { + vpc_id = aws_vpc.voting-app-vpc.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_nat_gateway.nat_gw.id + } + tags = { + Name = "private_route" + } +} + +resource "aws_route_table_association" "private_association" { + subnet_id = aws_subnet.private_subnet.id + route_table_id = aws_route_table.private_route.id +} \ No newline at end of file diff --git a/terraform/terraform.tfvars b/terraform/terraform.tfvars new file mode 100644 index 0000000000..86e9bceb15 --- /dev/null +++ b/terraform/terraform.tfvars @@ -0,0 +1,14 @@ +region = "us-east-1" + +availability_zone = { + public_subnet_az = "us-east-1a" + private_subnet_az = "us-east-1b" +} + +cidr_block = "10.0.0.0/16" +vpc_name = "voting-app-vpc" +public_subnet_cidr_block = "10.0.1.0/24" +private_subnet_cidr_block = "10.0.2.0/24" +ami_image = "ami-0e2c8caa4b6378d8c" +instance_type = "t2.micro" +inbound_rule = "yes" \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000000..f8bdad3dba --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,65 @@ +variable "cidr_block" { + type = string + description = "CIDR block for VPC" + default = "10.0.0.0/16" +} + +variable "voting-app-vpc" { + type = string + description = "voting-app-vpc" + default = "yes" +} + +variable "vpc_name" { + type = string + description = "vpc name" + default = "voting-app-vpc" +} + +variable "public_subnet_cidr_block" { + type = string + default = "10.0.1.0/24" + description = "cidr range for public subnet" +} + +variable "private_subnet_cidr_block" { + type = string + default = "10.0.2.0/24" + description = "cidr range for private subnet" +} + +variable "region" { + type = string + default = "us-east-1" + description = "default region where infrastructures will be provisioned" +} + +variable "availability_zone" { + type = map(string) + default = { + public_subnet_az = "us-east-1a" + private_subnet_az = "us-east-1b" + } +} + +variable "ami_image" { + type = string + default = "ami-0e2c8caa4b6378d8c" +} + +variable "instance_type" { + type = string + default = "t2.micro" +} + +variable "inbound_rule" { + description = "Enable inboud rule" + type = string + default = "yes" +} + +variable "subnets" { + description = "List of subnets" + type = list(string) + default = ["public_subnet", "private_subnet"] +} \ No newline at end of file From 35049b9bf2dd84f1713e21221ef30d60238f98cb Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Thu, 19 Dec 2024 15:57:23 +0100 Subject: [PATCH 06/21] adding terraform and ansible configurations --- ansible/playbook.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/ansible/playbook.yml b/ansible/playbook.yml index 0b7a3bf59c..aa9931ae39 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook.yml @@ -2,12 +2,18 @@ - hosts: vote become: true tasks: - - name: Install Docker + - name: Install Docker on all hosts apt: name: docker.io state: present update_cache: yes + - name: Start Docker service + service: + name: docker + state: started + enabled: yes + - name: Pull Docker image for "vote" docker_image: name: ghenac/voting-app @@ -15,7 +21,7 @@ tag: vote when: inventory_hostname == 'vote' - - name: Run vote service + - name: Run "vote" container docker_container: name: vote-service image: ghenac/voting-app:vote @@ -32,7 +38,7 @@ tag: result when: inventory_hostname == 'result' - - name: Run result service + - name: Run "result" container docker_container: name: result-service image: ghenac/voting-app:result @@ -49,7 +55,7 @@ tag: latest when: inventory_hostname == 'worker' - - name: Run worker service + - name: Run "worker" container docker_container: name: worker-service image: dockersamples/examplevotingapp_worker:latest From 23d47517c2b8428d99601dea751e91f2f0a8717d Mon Sep 17 00:00:00 2001 From: InstructorDiogo Date: Sun, 22 Dec 2024 14:42:00 +0100 Subject: [PATCH 07/21] fixing worker --- worker/Dockerfile | 56 ++++++++++++++++++++++++++++++++++++++++------- worker/Program.cs | 28 +++++++++++++++++++----- 2 files changed, 70 insertions(+), 14 deletions(-) diff --git a/worker/Dockerfile b/worker/Dockerfile index 118aebf072..e4c67dec8a 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -1,17 +1,57 @@ -# Stage 1: Build +# # Stage 1: Build +# FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +# WORKDIR /source + +# # Copy the project file and restore dependencies +# COPY *.csproj ./ +# RUN dotnet restore + +# # Copy the remaining source code and publish +# COPY . ./ +# RUN dotnet publish -c Release -o /app --runtime linux-arm64 --self-contained false --no-restore + +# # Stage 2: Runtime +# FROM mcr.microsoft.com/dotnet/runtime:7.0 +# WORKDIR /app +# COPY --from=build /app . +# ENTRYPOINT ["dotnet", "Worker.dll"] + +# Stage 1: Build the application FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build -WORKDIR /source + +# Set the working directory inside the container +WORKDIR /src # Copy the project file and restore dependencies -COPY *.csproj ./ +# Adjust 'Worker.csproj' if your project file has a different name +COPY Worker.csproj ./ RUN dotnet restore -# Copy the remaining source code and publish +# Copy the entire source code into the container COPY . ./ -RUN dotnet publish -c Release -o /app --runtime linux-arm64 --self-contained false --no-restore -# Stage 2: Runtime -FROM mcr.microsoft.com/dotnet/runtime:7.0 +# Publish the application in Release mode to the /app/publish directory +RUN dotnet publish -c Release -o /app/publish + +# Stage 2: Create the runtime image +FROM mcr.microsoft.com/dotnet/runtime:7.0 AS runtime + +# Set the working directory inside the runtime container WORKDIR /app -COPY --from=build /app . + +# Copy the published output from the build stage +COPY --from=build /app/publish . + +# (Optional) Set environment variables with default values +# These can be overridden at runtime using Docker's `-e` flag or Docker Compose +ENV DB_HOST=db +ENV DB_USERNAME=postgres +ENV DB_PASSWORD=postgres +ENV DB_NAME=postgres +ENV REDIS_HOST=redis + +# (Optional) Expose ports if your application listens on any (not required for background workers) +# EXPOSE 80 + +# Define the entry point for the container to run the application ENTRYPOINT ["dotnet", "Worker.dll"] diff --git a/worker/Program.cs b/worker/Program.cs index 9b5fb74d1a..d2b6b84a6e 100644 --- a/worker/Program.cs +++ b/worker/Program.cs @@ -16,8 +16,21 @@ public static int Main(string[] args) { try { - var pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); - var redisConn = OpenRedisConnection("redis"); + // Fetch configuration from environment variables + var dbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "db"; + var dbUsername = Environment.GetEnvironmentVariable("DB_USERNAME") ?? "postgres"; + var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "postgres"; + var dbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "postgres"; + + var redisHost = Environment.GetEnvironmentVariable("REDIS_HOST") ?? "redis"; + + // Construct the connection strings + var pgConnectionString = $"Server={dbHost};Username={dbUsername};Password={dbPassword};Database={dbName}"; + Console.WriteLine($"Database connection string: {pgConnectionString}"); + var redisConnectionString = redisHost; + + var pgsql = OpenDbConnection(pgConnectionString); + var redisConn = OpenRedisConnection(redisConnectionString); var redis = redisConn.GetDatabase(); // Keep alive is not implemented in Npgsql yet. This workaround was recommended: @@ -32,21 +45,24 @@ public static int Main(string[] args) Thread.Sleep(100); // Reconnect redis if down - if (redisConn == null || !redisConn.IsConnected) { + if (redisConn == null || !redisConn.IsConnected) + { Console.WriteLine("Reconnecting Redis"); - redisConn = OpenRedisConnection("redis"); + redisConn = OpenRedisConnection(redisConnectionString); redis = redisConn.GetDatabase(); } + string json = redis.ListLeftPopAsync("votes").Result; if (json != null) { var vote = JsonConvert.DeserializeAnonymousType(json, definition); Console.WriteLine($"Processing vote for '{vote.vote}' by '{vote.voter_id}'"); + // Reconnect DB if down if (!pgsql.State.Equals(System.Data.ConnectionState.Open)) { Console.WriteLine("Reconnecting DB"); - pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); + pgsql = OpenDbConnection(pgConnectionString); } else { // Normal +1 vote requested @@ -151,4 +167,4 @@ private static void UpdateVote(NpgsqlConnection connection, string voterId, stri } } } -} \ No newline at end of file +} From d347eb9ce6f8bb375d5f1fc70d5168fce0e57572 Mon Sep 17 00:00:00 2001 From: InstructorDiogo Date: Sun, 22 Dec 2024 14:45:43 +0100 Subject: [PATCH 08/21] project-1-fixed --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 070e370843..6ca2914bc0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,7 @@ services: - back-tier worker: - image: dockersamples/examplevotingapp_worker + build: ./worker depends_on: redis: condition: service_healthy From 5ca75b37e06e23a0f9322fe3a6246f75c693ed31 Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Sun, 22 Dec 2024 14:46:18 +0100 Subject: [PATCH 09/21] ansible and terraform --- ansible/backend.yml | 60 ++++++++++++++ ansible/db.yml | 41 ++++++++++ ansible/frontend.yml | 85 ++++++++++++++++++++ ansible/inventory.ini | 8 ++ ansible/main.yml | 4 + ansible/{playbook.yml => playbook_v1.yml} | 2 + ansible/playbook_v2.yml | 97 +++++++++++++++++++++++ 7 files changed, 297 insertions(+) create mode 100644 ansible/backend.yml create mode 100644 ansible/db.yml create mode 100644 ansible/frontend.yml create mode 100644 ansible/inventory.ini create mode 100644 ansible/main.yml rename ansible/{playbook.yml => playbook_v1.yml} (97%) create mode 100644 ansible/playbook_v2.yml diff --git a/ansible/backend.yml b/ansible/backend.yml new file mode 100644 index 0000000000..fd07f9321e --- /dev/null +++ b/ansible/backend.yml @@ -0,0 +1,60 @@ +--- +- name: Ensure Docker is installed and running on the backend instance + hosts: backend + become: yes + tasks: + - name: Update apt package index + ansible.builtin.apt: + update_cache: yes + + - name: Install Docker if not installed + ansible.builtin.apt: + name: docker.io + state: present + + - name: Ensure Docker service is running + ansible.builtin.service: + name: docker + state: started + enabled: true + +##### Worker Service ##### + - name: Pull the vote Docker image + ansible.builtin.docker_image: + name: dockersamples/examplevotingapp_worker + tag: latest + source: pull + + - name: Run the worker-app container + ansible.builtin.docker_container: + name: voting-app-vote + image: dockersamples/examplevotingapp_worker:latest + state: started + restart_policy: always + + - name: Verify the container is running + ansible.builtin.docker_container_info: + name: worker-app + register: container_info + +##### Redis Service ##### + - name: Pull the Redis Docker image + ansible.builtin.docker_image: + name: redis:alpine + source: pull + + - name: Run the Redis container + ansible.builtin.docker_container: + name: Redis + image: redis:alpine + state: started + restart_policy: always + exposed_ports: + - "6379" + published_ports: + - "6379:6379" + + - name: Verify the container is running + ansible.builtin.docker_container_info: + name: Redis + register: container_info \ No newline at end of file diff --git a/ansible/db.yml b/ansible/db.yml new file mode 100644 index 0000000000..2db7fe518a --- /dev/null +++ b/ansible/db.yml @@ -0,0 +1,41 @@ +--- +- name: Ensure Docker is installed and running on the DB instance + hosts: db + become: yes + tasks: + - name: Update apt package index + ansible.builtin.apt: + update_cache: yes + + - name: Install Docker if not installed + ansible.builtin.apt: + name: docker.io + state: present + + - name: Ensure Docker service is running + ansible.builtin.service: + name: docker + state: started + enabled: true + +##### DB service ##### + - name: Pull the DB Docker image + ansible.builtin.docker_image: + name: postgres:15-alpine + source: pull + + - name: Run the DB container + ansible.builtin.docker_container: + name: voting-app-db + image: postgres:15-alpine + state: started + restart_policy: always + exposed_ports: + - "5432" + published_ports: + - "5432:5432" + + - name: Verify the container is running + ansible.builtin.docker_container_info: + name: voting-app-db + register: container_info \ No newline at end of file diff --git a/ansible/frontend.yml b/ansible/frontend.yml new file mode 100644 index 0000000000..ffcb0424ec --- /dev/null +++ b/ansible/frontend.yml @@ -0,0 +1,85 @@ +--- +- name: Ensure Docker is installed and running on the frontend instance + hosts: frontend + become: yes + tasks: + - name: Update apt package index + ansible.builtin.apt: + update_cache: yes + + - name: Install Docker if not installed + ansible.builtin.apt: + name: docker.io + state: present + + - name: Ensure Docker service is running + ansible.builtin.service: + name: docker + state: started + enabled: true + +##### Vote Service ##### + - name: Pull the vote Docker image + ansible.builtin.docker_image: + name: ghenac/voting-app + tag: vote + source: pull + + - name: Run the 'vote' container + ansible.builtin.docker_container: + name: voting-app-vote + image: ghenac/voting-app:vote + state: started + restart_policy: always + exposed_ports: + - "80" + published_ports: + - "8080:80" + + - name: Verify the container is running + ansible.builtin.docker_container_info: + name: voting-app-vote + register: container_info + + +##### Result Service ##### +- name: Ensure Docker is installed and running on the vote instance + hosts: frontend + become: yes + tasks: + - name: Update apt package index + ansible.builtin.apt: + update_cache: yes + + - name: Install Docker if not installed + ansible.builtin.apt: + name: docker.io + state: present + + - name: Ensure Docker service is running + ansible.builtin.service: + name: docker + state: started + enabled: true + + - name: Pull the result Docker image + ansible.builtin.docker_image: + name: ghenac/voting-app + tag: result + source: pull + + - name: Run the 'result' container + ansible.builtin.docker_container: + name: voting-app-result + image: ghenac/voting-app:result + state: started + restart_policy: always + exposed_ports: + - "80" + published_ports: + - "8081:80" + + - name: Verify the container is running + ansible.builtin.docker_container_info: + name: voting-app-result + register: container_info \ No newline at end of file diff --git a/ansible/inventory.ini b/ansible/inventory.ini new file mode 100644 index 0000000000..2efd39ce9b --- /dev/null +++ b/ansible/inventory.ini @@ -0,0 +1,8 @@ +[frontend] +54.147.246.251 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu + +[backend] +35.172.222.148 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu + +[db] +34.228.60.174 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu diff --git a/ansible/main.yml b/ansible/main.yml new file mode 100644 index 0000000000..343380fc31 --- /dev/null +++ b/ansible/main.yml @@ -0,0 +1,4 @@ +--- +- import_playbook: frontend.yml +- import_playbook: backend.yml +- import_playbook: db.yml diff --git a/ansible/playbook.yml b/ansible/playbook_v1.yml similarity index 97% rename from ansible/playbook.yml rename to ansible/playbook_v1.yml index aa9931ae39..34b28343e7 100644 --- a/ansible/playbook.yml +++ b/ansible/playbook_v1.yml @@ -29,6 +29,8 @@ - "8080:8080" state: started when: inventory_hostname == 'vote' + env: + REDIS_HOST: "redis.internal" ##### Result Server ##### - name: Pull Docker image for "result" diff --git a/ansible/playbook_v2.yml b/ansible/playbook_v2.yml new file mode 100644 index 0000000000..56b7d05a42 --- /dev/null +++ b/ansible/playbook_v2.yml @@ -0,0 +1,97 @@ +--- +- name: Configure and deploy Voting App + hosts: all + become: true + vars: + vote_app_image: "ghenac/voting-app:vote" + result_app_image: "ghenac/voting-app:result" + worker_app_image: "dockersamples/examplevotingapp_worker:latest" + + vote_port: "8080" + result_port: "8081" + + tasks: +##### Install Docker on EC2 Instances ##### + - name: Install Docker on all instances + apt: + name: docker.io + state: present + update_cache: yes + + - name: Add user to Docker group + user: + name: "{{ ansible_user }}" + groups: docker + append: yes + + - name: Start Docker service + service: + name: docker + state: started + enabled: yes + + - name: Verify Docker installation + command: docker version + register: docker_status + + - name: Debug Docker installation output + debug: + msg: "{{ docker_status.stdout_lines }}" + +##### Pull and Run Containers ##### + + # Vote Service # + - name: Pull Vote App image + docker_image: + name: "{{ vote_app_image }}" + source: pull + + - name: Run the Vote App container + docker_container: + name: vote-service + image: "{{ vote_app_image }}" + ports: + - "{{ vote_port }}:80" + state: started + restart_policy: always + env: + REDIS_HOST: "redis.internal" + + # Result Service # + - name: Pull Result App image + docker_image: + name: "{{ result_app_image }}" + source: pull + + - name: Run the Result App container + docker_container: + name: result-service + image: "{{ result_app_image }}" + ports: + - "{{ result_port }}:80" + state: started + restart_policy: always + env: + DATABASE_HOST: "db.internal" + + # Worker Service # + - name: Pull Worker App image + docker_image: + name: "{{ worker_app_image }}" + source: pull + + - name: Run the Worker App container + docker_container: + name: worker-service + image: "{{ worker_app_image }}" + state: started + restart_policy: always + + ##### Verify Deployment ##### + - name: List running Docker containers + shell: docker ps + register: running_containers + + - name: Display running containers + debug: + msg: "{{ running_containers.stdout_lines }}" From f86b4f10fbf171f9890b20422d2a797ffa10847a Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Sun, 22 Dec 2024 14:48:06 +0100 Subject: [PATCH 10/21] new ansible upgrades --- terraform/alb.tf | 76 ++++++++++++++++++++++---------------------- terraform/main.tf | 30 +++++++++++++---- terraform/outputs.tf | 18 +++++------ worker/Dockerfile | 32 +++++++++++++++++++ worker/Program.cs | 28 ++++++++++++---- 5 files changed, 124 insertions(+), 60 deletions(-) diff --git a/terraform/alb.tf b/terraform/alb.tf index 9055197127..b9a094bdc5 100644 --- a/terraform/alb.tf +++ b/terraform/alb.tf @@ -21,39 +21,39 @@ resource "aws_lb" "voting_app_alb" { ##### Target Groups ##### -# Target group for 'vote' service # -resource "aws_lb_target_group" "vote_target_group" { - name = "vote-tg" +# Target group for "frontend" service # +resource "aws_lb_target_group" "frontend_target_group" { + name = "frontend-tg" port = 80 protocol = "HTTP" vpc_id = aws_vpc.voting-app-vpc.id target_type = "instance" tags = { - Name = "vote-tg" + Name = "frontend-tg" } } -# Target group for 'result' service # -resource "aws_lb_target_group" "result_target_group" { - name = "result-tg" +# Target group for "backend" service # +resource "aws_lb_target_group" "backend_target_group" { + name = "backend-tg" port = 80 protocol = "HTTP" vpc_id = aws_vpc.voting-app-vpc.id target_type = "instance" tags = { - Name = "result-tg" + Name = "backend-tg" } } -# Target group for 'worker' service # -resource "aws_lb_target_group" "worker_target_group" { - name = "worker-tg" +# Target group for "DB" service # +resource "aws_lb_target_group" "db_target_group" { + name = "db-tg" port = 80 protocol = "HTTP" vpc_id = aws_vpc.voting-app-vpc.id target_type = "instance" tags = { - Name = "worker-tg" + Name = "db-tg" } } @@ -77,85 +77,85 @@ resource "aws_lb_listener" "voting_app_listener" { ##### Listener Rules ##### -# Rule for 'vote' traffic # -resource "aws_lb_listener_rule" "vote_rule" { +# Rule for "frontend" traffic # +resource "aws_lb_listener_rule" "frontend_rule" { listener_arn = aws_lb_listener.voting_app_listener.arn priority = 1 tags = { - Name = "vote" + Name = "frontend" } condition { host_header { - values = ["vote.example.com"] + values = ["frontend.example.com"] } } action { type = "forward" - target_group_arn = aws_lb_target_group.vote_target_group.arn + target_group_arn = aws_lb_target_group.frontend_target_group.arn } } -# Rule for 'result' traffic # -resource "aws_lb_listener_rule" "result_rule" { +# Rule for "backend" traffic # +resource "aws_lb_listener_rule" "backend_rule" { listener_arn = aws_lb_listener.voting_app_listener.arn priority = 2 tags = { - Name = "result" + Name = "backend" } condition { host_header { - values = ["result.example.com"] + values = ["backend.example.com"] } } action { type = "forward" - target_group_arn = aws_lb_target_group.result_target_group.arn + target_group_arn = aws_lb_target_group.backend_target_group.arn } } -# Rule for 'worker' traffic # -resource "aws_lb_listener_rule" "worker_rule" { +# Rule for "DB" traffic # +resource "aws_lb_listener_rule" "db_rule" { listener_arn = aws_lb_listener.voting_app_listener.arn priority = 3 tags = { - Name = "worker" + Name = "db" } condition { host_header { - values = ["worker.example.com"] + values = ["db.example.com"] } } action { type = "forward" - target_group_arn = aws_lb_target_group.worker_target_group.arn + target_group_arn = aws_lb_target_group.db_target_group.arn } } ##### Attach Instances to Target Groups ##### -# Register 'vote' instance # -resource "aws_lb_target_group_attachment" "vote_attachment" { - target_group_arn = aws_lb_target_group.vote_target_group.arn - target_id = aws_instance.vote.id +# Register "frontend" instance # +resource "aws_lb_target_group_attachment" "frontend_attachment" { + target_group_arn = aws_lb_target_group.frontend_target_group.arn + target_id = aws_instance.frontend.id port = 80 } -# Register 'result' instance # -resource "aws_lb_target_group_attachment" "result_attachment" { - target_group_arn = aws_lb_target_group.result_target_group.arn - target_id = aws_instance.result.id +# Register "backend" instance # +resource "aws_lb_target_group_attachment" "backend_attachment" { + target_group_arn = aws_lb_target_group.backend_target_group.arn + target_id = aws_instance.backend.id port = 80 } -# Register 'worker' instance # +# Register "DB" instance # resource "aws_lb_target_group_attachment" "worker_attachment" { - target_group_arn = aws_lb_target_group.worker_target_group.arn - target_id = aws_instance.worker.id + target_group_arn = aws_lb_target_group.db_target_group.arn + target_id = aws_instance.db.id port = 80 } diff --git a/terraform/main.tf b/terraform/main.tf index 4f175c2132..c0f96af693 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -22,7 +22,7 @@ resource "aws_vpc" "voting-app-vpc" { } ##### EC2 Instances ##### -resource "aws_instance" "vote" { +resource "aws_instance" "frontend" { ami = var.ami_image instance_type = var.instance_type key_name = "voting-app-key" @@ -31,11 +31,11 @@ resource "aws_instance" "vote" { subnet_id = aws_subnet.public_subnet.id associate_public_ip_address = true tags = { - Name = "vote" + Name = "frontend" } } -resource "aws_instance" "result" { +resource "aws_instance" "backend" { ami = var.ami_image instance_type = var.instance_type key_name = "voting-app-key" @@ -44,11 +44,11 @@ resource "aws_instance" "result" { subnet_id = aws_subnet.public_subnet.id associate_public_ip_address = true tags = { - Name = "result" + Name = "backend" } } -resource "aws_instance" "worker" { +resource "aws_instance" "db" { ami = var.ami_image instance_type = var.instance_type key_name = "voting-app-key" @@ -57,7 +57,7 @@ resource "aws_instance" "worker" { subnet_id = aws_subnet.public_subnet.id associate_public_ip_address = true tags = { - Name = "worker" + Name = "db" } } @@ -109,6 +109,22 @@ resource "aws_security_group" "public_security_group" { cidr_blocks = ["0.0.0.0/0"] } + ingress { + description = "Http from Internet" + from_port = 8080 + to_port = 8080 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "Http from Internet" + from_port = 8081 + to_port = 8081 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + egress { from_port = 0 to_port = 0 @@ -123,7 +139,7 @@ resource "aws_security_group" "public_security_group" { # Private # resource "aws_security_group" "private_security_group" { name = "voting-app_private-sec-gr" - description = "Allow traffic on private VM" + description = "Allow private traffic " vpc_id = aws_vpc.voting-app-vpc.id ingress { diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 1fa3da4efe..b5f89dd268 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -1,17 +1,17 @@ ##### Outputs for EC2 Instances ##### -output "vote_instance_public_ip" { - description = "The public IP address of the 'vote' EC2 instance" - value = aws_instance.vote.public_ip +output "frontend_instance_public_ip" { + description = "The public IP address of the 'frontend' EC2 instance" + value = aws_instance.frontend.public_ip } -output "result_instance_public_ip" { - description = "The public IP address of the 'result' EC2 instance" - value = aws_instance.result.public_ip +output "backend_instance_public_ip" { + description = "The public IP address of the 'backend' EC2 instance" + value = aws_instance.backend.public_ip } -output "worker_instance_public_ip" { - description = "The public IP address of the 'worker' EC2 instance" - value = aws_instance.worker.public_ip +output "db_instance_public_ip" { + description = "The public IP address of the 'DB' EC2 instance" + value = aws_instance.db.public_ip } ##### Outputs for Subnets ##### diff --git a/worker/Dockerfile b/worker/Dockerfile index 118aebf072..8d1bb952af 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -15,3 +15,35 @@ FROM mcr.microsoft.com/dotnet/runtime:7.0 WORKDIR /app COPY --from=build /app . ENTRYPOINT ["dotnet", "Worker.dll"] + + + +##### Dockerfile V2 ##### + +# Stage 1: Build +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /app + +# Copy and restore project files +COPY ./*.csproj ./ +RUN dotnet restore + +# Copy all source files and build +COPY . ./ +RUN dotnet publish -c Release -o /app/out + +# Stage 2: Runtime +FROM mcr.microsoft.com/dotnet/runtime:7.0 +WORKDIR /app + +# Copy the build output +COPY --from=build /app/out ./ + +# Expose port if necessary (e.g., for health checks or specific APIs) +EXPOSE 80 + +# Environment variables for runtime (customize as needed) +ENV DOTNET_EnableDiagnostics=0 + +# Command to run the application +CMD ["dotnet", "Worker.dll"] diff --git a/worker/Program.cs b/worker/Program.cs index 9b5fb74d1a..d2b6b84a6e 100644 --- a/worker/Program.cs +++ b/worker/Program.cs @@ -16,8 +16,21 @@ public static int Main(string[] args) { try { - var pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); - var redisConn = OpenRedisConnection("redis"); + // Fetch configuration from environment variables + var dbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "db"; + var dbUsername = Environment.GetEnvironmentVariable("DB_USERNAME") ?? "postgres"; + var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? "postgres"; + var dbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "postgres"; + + var redisHost = Environment.GetEnvironmentVariable("REDIS_HOST") ?? "redis"; + + // Construct the connection strings + var pgConnectionString = $"Server={dbHost};Username={dbUsername};Password={dbPassword};Database={dbName}"; + Console.WriteLine($"Database connection string: {pgConnectionString}"); + var redisConnectionString = redisHost; + + var pgsql = OpenDbConnection(pgConnectionString); + var redisConn = OpenRedisConnection(redisConnectionString); var redis = redisConn.GetDatabase(); // Keep alive is not implemented in Npgsql yet. This workaround was recommended: @@ -32,21 +45,24 @@ public static int Main(string[] args) Thread.Sleep(100); // Reconnect redis if down - if (redisConn == null || !redisConn.IsConnected) { + if (redisConn == null || !redisConn.IsConnected) + { Console.WriteLine("Reconnecting Redis"); - redisConn = OpenRedisConnection("redis"); + redisConn = OpenRedisConnection(redisConnectionString); redis = redisConn.GetDatabase(); } + string json = redis.ListLeftPopAsync("votes").Result; if (json != null) { var vote = JsonConvert.DeserializeAnonymousType(json, definition); Console.WriteLine($"Processing vote for '{vote.vote}' by '{vote.voter_id}'"); + // Reconnect DB if down if (!pgsql.State.Equals(System.Data.ConnectionState.Open)) { Console.WriteLine("Reconnecting DB"); - pgsql = OpenDbConnection("Server=db;Username=postgres;Password=postgres;"); + pgsql = OpenDbConnection(pgConnectionString); } else { // Normal +1 vote requested @@ -151,4 +167,4 @@ private static void UpdateVote(NpgsqlConnection connection, string voterId, stri } } } -} \ No newline at end of file +} From cb58f7ac92c387a5359a228a0771871731118838 Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Sun, 22 Dec 2024 17:25:10 +0100 Subject: [PATCH 11/21] updates to ansible DB configuration + terraform db update --- ansible/db.yml | 44 +++++++++++++++++++++++++++++++++++--------- terraform/main.tf | 8 ++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/ansible/db.yml b/ansible/db.yml index 2db7fe518a..750dcd5e25 100644 --- a/ansible/db.yml +++ b/ansible/db.yml @@ -2,6 +2,11 @@ - name: Ensure Docker is installed and running on the DB instance hosts: db become: yes + vars: + postgres_password: "YourSecurePassword" # Replace with a secure password or use Ansible Vault + postgres_db: "voting_app_db" + postgres_user: "voting_user" + postgres_port: 5432 tasks: - name: Update apt package index ansible.builtin.apt: @@ -12,30 +17,51 @@ name: docker.io state: present - - name: Ensure Docker service is running + - name: Ensure Docker service is running and enabled ansible.builtin.service: name: docker state: started enabled: true -##### DB service ##### - - name: Pull the DB Docker image - ansible.builtin.docker_image: + - name: Pull the PostgreSQL Docker image + community.docker.docker_image: name: postgres:15-alpine source: pull - - name: Run the DB container - ansible.builtin.docker_container: + - name: Create Docker network (optional, for better isolation) + community.docker.docker_network: + name: voting_app_network + state: present + + ##### DB service ##### + - name: Run the PostgreSQL container + community.docker.docker_container: name: voting-app-db image: postgres:15-alpine state: started restart_policy: always + env: + POSTGRES_PASSWORD: "postgres" + POSTGRES_DB: "voting-app-db" + POSTGRES_USER: "postgres" exposed_ports: - "5432" published_ports: - "5432:5432" + networks: + - name: voting_app_network + volumes: + - name: pgdata + source: pgdata + target: /var/lib/postgresql/data + type: volume + state: present - - name: Verify the container is running - ansible.builtin.docker_container_info: + - name: Verify the PostgreSQL container is running + community.docker.docker_container_info: name: voting-app-db - register: container_info \ No newline at end of file + register: container_info + + - name: Debug container information + debug: + var: container_info diff --git a/terraform/main.tf b/terraform/main.tf index c0f96af693..72d9ac837a 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -134,6 +134,14 @@ resource "aws_security_group" "public_security_group" { tags = { Name = "voting-app_public-sec-gr" } + + ingress { + description = "PostgreSQL from anywhere" + from_port = 5432 + to_port = 5432 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } } # Private # From 60aa95278135a4fb9e8ebe1d2d2347532d04dcec Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Sun, 22 Dec 2024 17:34:36 +0100 Subject: [PATCH 12/21] just some adjustments and cleaning --- ansible/backend.yml | 5 ++- ansible/db.yml | 4 ++ ansible/frontend.yml | 5 ++- ansible/playbook_v1.yml | 65 --------------------------- ansible/playbook_v2.yml | 97 ----------------------------------------- 5 files changed, 12 insertions(+), 164 deletions(-) delete mode 100644 ansible/playbook_v1.yml delete mode 100644 ansible/playbook_v2.yml diff --git a/ansible/backend.yml b/ansible/backend.yml index fd07f9321e..d802bc9dbe 100644 --- a/ansible/backend.yml +++ b/ansible/backend.yml @@ -57,4 +57,7 @@ - name: Verify the container is running ansible.builtin.docker_container_info: name: Redis - register: container_info \ No newline at end of file + register: container_info + + +# Remember to add configuration to change mode for the user in the EC2 instance for Docker # \ No newline at end of file diff --git a/ansible/db.yml b/ansible/db.yml index 750dcd5e25..b9bba8af0a 100644 --- a/ansible/db.yml +++ b/ansible/db.yml @@ -65,3 +65,7 @@ - name: Debug container information debug: var: container_info + + + +# Remember to add configuration to change mode for the user in the EC2 instance for Docker # \ No newline at end of file diff --git a/ansible/frontend.yml b/ansible/frontend.yml index ffcb0424ec..306e48a665 100644 --- a/ansible/frontend.yml +++ b/ansible/frontend.yml @@ -82,4 +82,7 @@ - name: Verify the container is running ansible.builtin.docker_container_info: name: voting-app-result - register: container_info \ No newline at end of file + register: container_info + + +# Remember to add configuration to change mode for the user in the EC2 instance for Docker # \ No newline at end of file diff --git a/ansible/playbook_v1.yml b/ansible/playbook_v1.yml deleted file mode 100644 index 34b28343e7..0000000000 --- a/ansible/playbook_v1.yml +++ /dev/null @@ -1,65 +0,0 @@ -##### Vote server ##### -- hosts: vote - become: true - tasks: - - name: Install Docker on all hosts - apt: - name: docker.io - state: present - update_cache: yes - - - name: Start Docker service - service: - name: docker - state: started - enabled: yes - - - name: Pull Docker image for "vote" - docker_image: - name: ghenac/voting-app - source: pull - tag: vote - when: inventory_hostname == 'vote' - - - name: Run "vote" container - docker_container: - name: vote-service - image: ghenac/voting-app:vote - ports: - - "8080:8080" - state: started - when: inventory_hostname == 'vote' - env: - REDIS_HOST: "redis.internal" - -##### Result Server ##### - - name: Pull Docker image for "result" - docker_image: - name: ghenac/voting-app - source: pull - tag: result - when: inventory_hostname == 'result' - - - name: Run "result" container - docker_container: - name: result-service - image: ghenac/voting-app:result - ports: - - "8081:8081" - state: started - when: inventory_hostname == 'result' - -##### Worker Server ##### - - name: Pull Docker image for "worker" - docker_image: - name: dockersamples/examplevotingapp_worker - source: pull - tag: latest - when: inventory_hostname == 'worker' - - - name: Run "worker" container - docker_container: - name: worker-service - image: dockersamples/examplevotingapp_worker:latest - state: started - when: inventory_hostname == 'worker' \ No newline at end of file diff --git a/ansible/playbook_v2.yml b/ansible/playbook_v2.yml deleted file mode 100644 index 56b7d05a42..0000000000 --- a/ansible/playbook_v2.yml +++ /dev/null @@ -1,97 +0,0 @@ ---- -- name: Configure and deploy Voting App - hosts: all - become: true - vars: - vote_app_image: "ghenac/voting-app:vote" - result_app_image: "ghenac/voting-app:result" - worker_app_image: "dockersamples/examplevotingapp_worker:latest" - - vote_port: "8080" - result_port: "8081" - - tasks: -##### Install Docker on EC2 Instances ##### - - name: Install Docker on all instances - apt: - name: docker.io - state: present - update_cache: yes - - - name: Add user to Docker group - user: - name: "{{ ansible_user }}" - groups: docker - append: yes - - - name: Start Docker service - service: - name: docker - state: started - enabled: yes - - - name: Verify Docker installation - command: docker version - register: docker_status - - - name: Debug Docker installation output - debug: - msg: "{{ docker_status.stdout_lines }}" - -##### Pull and Run Containers ##### - - # Vote Service # - - name: Pull Vote App image - docker_image: - name: "{{ vote_app_image }}" - source: pull - - - name: Run the Vote App container - docker_container: - name: vote-service - image: "{{ vote_app_image }}" - ports: - - "{{ vote_port }}:80" - state: started - restart_policy: always - env: - REDIS_HOST: "redis.internal" - - # Result Service # - - name: Pull Result App image - docker_image: - name: "{{ result_app_image }}" - source: pull - - - name: Run the Result App container - docker_container: - name: result-service - image: "{{ result_app_image }}" - ports: - - "{{ result_port }}:80" - state: started - restart_policy: always - env: - DATABASE_HOST: "db.internal" - - # Worker Service # - - name: Pull Worker App image - docker_image: - name: "{{ worker_app_image }}" - source: pull - - - name: Run the Worker App container - docker_container: - name: worker-service - image: "{{ worker_app_image }}" - state: started - restart_policy: always - - ##### Verify Deployment ##### - - name: List running Docker containers - shell: docker ps - register: running_containers - - - name: Display running containers - debug: - msg: "{{ running_containers.stdout_lines }}" From 47dfc59f5f3c93bfa0cf223b490a63725f5e4e00 Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Sun, 22 Dec 2024 18:52:17 +0100 Subject: [PATCH 13/21] Ansible redis and worker setup; Change user mode task added in all playbooks. --- ansible/backend.yml | 65 ++++++++++++++++++++++++++++--------------- ansible/db.yml | 18 ++++++------ ansible/frontend.yml | 11 ++++++-- ansible/inventory.ini | 2 +- worker/Dockerfile | 34 +++++++++++----------- 5 files changed, 76 insertions(+), 54 deletions(-) diff --git a/ansible/backend.yml b/ansible/backend.yml index d802bc9dbe..ea1e57947d 100644 --- a/ansible/backend.yml +++ b/ansible/backend.yml @@ -12,32 +12,25 @@ name: docker.io state: present + - name: Create backend Docker network if it does not exist + ansible.builtin.docker_network: + name: backend-network + state: present + + - name: Add the user to the Docker group + ansible.builtin.user: + name: "ubuntu" + groups: docker + append: yes + state: present + - name: Ensure Docker service is running ansible.builtin.service: name: docker state: started enabled: true -##### Worker Service ##### - - name: Pull the vote Docker image - ansible.builtin.docker_image: - name: dockersamples/examplevotingapp_worker - tag: latest - source: pull - - - name: Run the worker-app container - ansible.builtin.docker_container: - name: voting-app-vote - image: dockersamples/examplevotingapp_worker:latest - state: started - restart_policy: always - - - name: Verify the container is running - ansible.builtin.docker_container_info: - name: worker-app - register: container_info - -##### Redis Service ##### + ##### Redis Service ##### - name: Pull the Redis Docker image ansible.builtin.docker_image: name: redis:alpine @@ -48,16 +41,42 @@ name: Redis image: redis:alpine state: started + networks: + - name: backend-network restart_policy: always exposed_ports: - "6379" published_ports: - "6379:6379" - - name: Verify the container is running + - name: Verify the Redis container is running ansible.builtin.docker_container_info: name: Redis - register: container_info + register: redis_container_info + + + ##### Worker Service ##### + - name: Pull the worker Docker image + ansible.builtin.docker_image: + name: dockersamples/examplevotingapp_worker + tag: latest + source: pull + - name: Run the worker-app container + ansible.builtin.docker_container: + name: voting-app-worker + image: dockersamples/examplevotingapp_worker:latest + state: started + networks: + - name: backend-network + restart_policy: always + env: + REDIS_HOST: "Redis" # This is how the worker will connect to Redis container by using the container name as hostname + REDIS_PORT: "6379" + volumes: + - /tmp:/tmp -# Remember to add configuration to change mode for the user in the EC2 instance for Docker # \ No newline at end of file + - name: Verify the worker container is running + ansible.builtin.docker_container_info: + name: voting-app-worker + register: worker_container_info \ No newline at end of file diff --git a/ansible/db.yml b/ansible/db.yml index b9bba8af0a..140c2af267 100644 --- a/ansible/db.yml +++ b/ansible/db.yml @@ -3,7 +3,7 @@ hosts: db become: yes vars: - postgres_password: "YourSecurePassword" # Replace with a secure password or use Ansible Vault + postgres_password: "postgres" postgres_db: "voting_app_db" postgres_user: "voting_user" postgres_port: 5432 @@ -17,6 +17,13 @@ name: docker.io state: present + - name: Add the user to the Docker group + ansible.builtin.user: + name: "ubuntu" + groups: docker + append: yes + state: present + - name: Ensure Docker service is running and enabled ansible.builtin.service: name: docker @@ -28,11 +35,6 @@ name: postgres:15-alpine source: pull - - name: Create Docker network (optional, for better isolation) - community.docker.docker_network: - name: voting_app_network - state: present - ##### DB service ##### - name: Run the PostgreSQL container community.docker.docker_container: @@ -65,7 +67,3 @@ - name: Debug container information debug: var: container_info - - - -# Remember to add configuration to change mode for the user in the EC2 instance for Docker # \ No newline at end of file diff --git a/ansible/frontend.yml b/ansible/frontend.yml index 306e48a665..f1e55b1abd 100644 --- a/ansible/frontend.yml +++ b/ansible/frontend.yml @@ -12,6 +12,13 @@ name: docker.io state: present + - name: Add the user to the Docker group + ansible.builtin.user: + name: "ubuntu" + groups: docker + append: yes + state: present + - name: Ensure Docker service is running ansible.builtin.service: name: docker @@ -83,6 +90,4 @@ ansible.builtin.docker_container_info: name: voting-app-result register: container_info - - -# Remember to add configuration to change mode for the user in the EC2 instance for Docker # \ No newline at end of file + \ No newline at end of file diff --git a/ansible/inventory.ini b/ansible/inventory.ini index 2efd39ce9b..bef51dfb8e 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -2,7 +2,7 @@ 54.147.246.251 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu [backend] -35.172.222.148 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu +44.223.35.102 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu [db] 34.228.60.174 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu diff --git a/worker/Dockerfile b/worker/Dockerfile index 8d1bb952af..9a0cff38da 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -1,20 +1,20 @@ -# Stage 1: Build -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build -WORKDIR /source - -# Copy the project file and restore dependencies -COPY *.csproj ./ -RUN dotnet restore - -# Copy the remaining source code and publish -COPY . ./ -RUN dotnet publish -c Release -o /app --runtime linux-arm64 --self-contained false --no-restore - -# Stage 2: Runtime -FROM mcr.microsoft.com/dotnet/runtime:7.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "Worker.dll"] +# # Stage 1: Build +# FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +# WORKDIR /source + +# # Copy the project file and restore dependencies +# COPY *.csproj ./ +# RUN dotnet restore + +# # Copy the remaining source code and publish +# COPY . ./ +# RUN dotnet publish -c Release -o /app --runtime linux-arm64 --self-contained false --no-restore + +# # Stage 2: Runtime +# FROM mcr.microsoft.com/dotnet/runtime:7.0 +# WORKDIR /app +# COPY --from=build /app . +# ENTRYPOINT ["dotnet", "Worker.dll"] From f0020fa1c75bf0be8e4c2a994153fc5dd32d4912 Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Mon, 23 Dec 2024 15:30:35 +0100 Subject: [PATCH 14/21] updated inventory.ini --- ansible/inventory.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ansible/inventory.ini b/ansible/inventory.ini index bef51dfb8e..d81a05989f 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -1,8 +1,8 @@ [frontend] -54.147.246.251 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu +54.147.170.240 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu [backend] 44.223.35.102 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu [db] -34.228.60.174 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu +3.91.95.245 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu From d39536205f8a1f103f5436ef22ee0d5c82b457ea Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Mon, 23 Dec 2024 16:53:27 +0100 Subject: [PATCH 15/21] updates on ansible and Dockerfiles and vote app.py --- ansible/frontend.yml | 4 +++ ansible/main.yml | 4 +-- result/server.js | 16 +++++++++-- vote/Dockerfile | 5 ++++ vote/app.py | 5 +++- worker/Dockerfile | 66 +++++++++++++++++++------------------------- 6 files changed, 56 insertions(+), 44 deletions(-) diff --git a/ansible/frontend.yml b/ansible/frontend.yml index f1e55b1abd..0847d5ae11 100644 --- a/ansible/frontend.yml +++ b/ansible/frontend.yml @@ -26,6 +26,7 @@ enabled: true ##### Vote Service ##### + - name: Pull the vote Docker image ansible.builtin.docker_image: name: ghenac/voting-app @@ -38,6 +39,9 @@ image: ghenac/voting-app:vote state: started restart_policy: always + env: + REDIS_HOST: "44.223.35.102" # This is how the worker will connect to Redis container by using the container name as hostname + REDIS_PORT: "6379" exposed_ports: - "80" published_ports: diff --git a/ansible/main.yml b/ansible/main.yml index 343380fc31..e5c3e82043 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -1,4 +1,4 @@ --- -- import_playbook: frontend.yml -- import_playbook: backend.yml - import_playbook: db.yml +- import_playbook: backend.yml +- import_playbook: frontend.yml diff --git a/result/server.js b/result/server.js index 1c8593e7ee..8dc829adb7 100644 --- a/result/server.js +++ b/result/server.js @@ -2,6 +2,7 @@ var express = require('express'), async = require('async'), { Pool } = require('pg'), cookieParser = require('cookie-parser'), + path = require('path'), app = express(), server = require('http').Server(app), io = require('socket.io')(server); @@ -17,8 +18,17 @@ io.on('connection', function (socket) { }); }); +// Build PostgreSQL connection string dynamically from environment variables +var pgHost = process.env.PG_HOST || 'localhost'; +var pgPort = process.env.PG_PORT || 5432; +var pgUser = process.env.PG_USER || 'postgres'; +var pgPassword = process.env.PG_PASSWORD || 'postgres'; +var pgDatabase = process.env.PG_DATABASE || 'postgres'; + +var connectionString = `postgresql://${pgUser}:${pgPassword}@${pgHost}:${pgPort}/${pgDatabase}`; + var pool = new Pool({ - connectionString: 'postgres://postgres:postgres@db/postgres' + connectionString: connectionString }); async.retry( @@ -64,7 +74,7 @@ function collectVotesFromResult(result) { } app.use(cookieParser()); -app.use(express.urlencoded()); +app.use(express.urlencoded({ extended: true })); app.use(express.static(__dirname + '/views')); app.get('/', function (req, res) { @@ -74,4 +84,4 @@ app.get('/', function (req, res) { server.listen(port, function () { var port = server.address().port; console.log('App running on port ' + port); -}); +}); \ No newline at end of file diff --git a/vote/Dockerfile b/vote/Dockerfile index 2681083600..da82be4e28 100644 --- a/vote/Dockerfile +++ b/vote/Dockerfile @@ -13,10 +13,15 @@ WORKDIR /usr/local/app COPY requirements.txt ./requirements.txt RUN pip install --no-cache-dir -r requirements.txt +# Set environment variables for Redis configuration (can be overridden at runtime) +ENV REDIS_HOST=redis +ENV REDIS_PORT=6379 + # dev defines a stage for development, where it'll watch for filesystem changes FROM base AS dev RUN pip install watchdog ENV FLASK_ENV=development + CMD ["python", "app.py"] # final defines the stage that will bundle the application for production diff --git a/vote/app.py b/vote/app.py index 596546612a..2272d10a93 100644 --- a/vote/app.py +++ b/vote/app.py @@ -6,8 +6,11 @@ import json import logging +# Read options and Redis configuration from environment variables option_a = os.getenv('OPTION_A', "Cats") option_b = os.getenv('OPTION_B', "Dogs") +redis_host = os.getenv('REDIS_HOST', 'redis') # Default to 'redis' for compatibility +redis_port = int(os.getenv('REDIS_PORT', 6379)) # Default Redis port is 6379 hostname = socket.gethostname() app = Flask(__name__) @@ -18,7 +21,7 @@ def get_redis(): if not hasattr(g, 'redis'): - g.redis = Redis(host="redis", db=0, socket_timeout=5) + g.redis = Redis(host=redis_host, port=redis_port, db=0, socket_timeout=5) return g.redis @app.route("/", methods=['POST','GET']) diff --git a/worker/Dockerfile b/worker/Dockerfile index 9a0cff38da..b006b59009 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -1,49 +1,39 @@ -# # Stage 1: Build -# FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build -# WORKDIR /source - -# # Copy the project file and restore dependencies -# COPY *.csproj ./ -# RUN dotnet restore - -# # Copy the remaining source code and publish -# COPY . ./ -# RUN dotnet publish -c Release -o /app --runtime linux-arm64 --self-contained false --no-restore - -# # Stage 2: Runtime -# FROM mcr.microsoft.com/dotnet/runtime:7.0 -# WORKDIR /app -# COPY --from=build /app . -# ENTRYPOINT ["dotnet", "Worker.dll"] - - - -##### Dockerfile V2 ##### - -# Stage 1: Build +# Stage 1: Build the application FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build -WORKDIR /app -# Copy and restore project files -COPY ./*.csproj ./ +# Set the working directory inside the container +WORKDIR /src + +# Copy the project file and restore dependencies +# Adjust 'Worker.csproj' if your project file has a different name +COPY Worker.csproj ./ RUN dotnet restore -# Copy all source files and build +# Copy the entire source code into the container COPY . ./ -RUN dotnet publish -c Release -o /app/out -# Stage 2: Runtime -FROM mcr.microsoft.com/dotnet/runtime:7.0 +# Publish the application in Release mode to the /app/publish directory +RUN dotnet publish -c Release -o /app/publish + +# Stage 2: Create the runtime image +FROM mcr.microsoft.com/dotnet/runtime:7.0 AS runtime + +# Set the working directory inside the runtime container WORKDIR /app -# Copy the build output -COPY --from=build /app/out ./ +# Copy the published output from the build stage +COPY --from=build /app/publish . -# Expose port if necessary (e.g., for health checks or specific APIs) -EXPOSE 80 +# (Optional) Set environment variables with default values +# These can be overridden at runtime using Docker's `-e` flag or Docker Compose +ENV DB_HOST=db +ENV DB_USERNAME=postgres +ENV DB_PASSWORD=postgres +ENV DB_NAME=postgres +ENV REDIS_HOST=redis -# Environment variables for runtime (customize as needed) -ENV DOTNET_EnableDiagnostics=0 +# (Optional) Expose ports if your application listens on any (not required for background workers) +# EXPOSE 80 -# Command to run the application -CMD ["dotnet", "Worker.dll"] +# Define the entry point for the container to run the application +ENTRYPOINT ["dotnet", "Worker.dll"] \ No newline at end of file From 7d1985f49cdcca67cfd100866dd9b84fef36b7ed Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Mon, 23 Dec 2024 17:35:11 +0100 Subject: [PATCH 16/21] updates on ansible configurations --- ansible/backend.yml | 9 +++++++-- ansible/frontend.yml | 19 +++++++++++++------ result/server.js | 2 ++ worker/Dockerfile | 33 +++++++++++---------------------- worker/Worker.csproj | 10 ++++++---- 5 files changed, 39 insertions(+), 34 deletions(-) diff --git a/ansible/backend.yml b/ansible/backend.yml index ea1e57947d..b542e885ee 100644 --- a/ansible/backend.yml +++ b/ansible/backend.yml @@ -58,14 +58,14 @@ ##### Worker Service ##### - name: Pull the worker Docker image ansible.builtin.docker_image: - name: dockersamples/examplevotingapp_worker + name: ghenac/voting-app-worker tag: latest source: pull - name: Run the worker-app container ansible.builtin.docker_container: name: voting-app-worker - image: dockersamples/examplevotingapp_worker:latest + image: ghenac/voting-app-worker:latest state: started networks: - name: backend-network @@ -73,6 +73,11 @@ env: REDIS_HOST: "Redis" # This is how the worker will connect to Redis container by using the container name as hostname REDIS_PORT: "6379" + DB_HOST: "3.91.95.245" + DB_USERNAME: "postgres" + DB_PASSWORD: "postgres" + DB_NAME: "voting-app-db" + volumes: - /tmp:/tmp diff --git a/ansible/frontend.yml b/ansible/frontend.yml index 0847d5ae11..d556fc5d55 100644 --- a/ansible/frontend.yml +++ b/ansible/frontend.yml @@ -29,14 +29,14 @@ - name: Pull the vote Docker image ansible.builtin.docker_image: - name: ghenac/voting-app - tag: vote + name: ghenac/voting-app-vote + tag: latest source: pull - name: Run the 'vote' container ansible.builtin.docker_container: name: voting-app-vote - image: ghenac/voting-app:vote + image: ghenac/voting-app-vote:latest state: started restart_policy: always env: @@ -75,20 +75,27 @@ - name: Pull the result Docker image ansible.builtin.docker_image: - name: ghenac/voting-app - tag: result + name: ghenac/voting-app-result + tag: latest source: pull - name: Run the 'result' container ansible.builtin.docker_container: name: voting-app-result - image: ghenac/voting-app:result + image: ghenac/voting-app-result:latest state: started restart_policy: always exposed_ports: - "80" published_ports: - "8081:80" + env: + PG_HOST: "3.91.95.245" + PG_PORT: "5432" + PG_USER: "postgres" + PG_PASSWORD: "postgres" + PG_DATABASE: "voting-app-db" + - name: Verify the container is running ansible.builtin.docker_container_info: diff --git a/result/server.js b/result/server.js index 8dc829adb7..5d4f95c8e3 100644 --- a/result/server.js +++ b/result/server.js @@ -27,6 +27,8 @@ var pgDatabase = process.env.PG_DATABASE || 'postgres'; var connectionString = `postgresql://${pgUser}:${pgPassword}@${pgHost}:${pgPort}/${pgDatabase}`; +console.log(connectionString) + var pool = new Pool({ connectionString: connectionString }); diff --git a/worker/Dockerfile b/worker/Dockerfile index b006b59009..919712525c 100644 --- a/worker/Dockerfile +++ b/worker/Dockerfile @@ -1,39 +1,28 @@ -# Stage 1: Build the application -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build - -# Set the working directory inside the container +# Stage 1: Build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src # Copy the project file and restore dependencies -# Adjust 'Worker.csproj' if your project file has a different name -COPY Worker.csproj ./ +COPY Worker.csproj . RUN dotnet restore -# Copy the entire source code into the container -COPY . ./ - -# Publish the application in Release mode to the /app/publish directory +# Copy the rest of the source code and build +COPY . . RUN dotnet publish -c Release -o /app/publish -# Stage 2: Create the runtime image -FROM mcr.microsoft.com/dotnet/runtime:7.0 AS runtime - -# Set the working directory inside the runtime container +# Stage 2: Runtime +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS runtime WORKDIR /app # Copy the published output from the build stage -COPY --from=build /app/publish . +COPY --from=build /app/publish ./ -# (Optional) Set environment variables with default values -# These can be overridden at runtime using Docker's `-e` flag or Docker Compose +# Set environment variables (can be overridden at runtime) ENV DB_HOST=db ENV DB_USERNAME=postgres ENV DB_PASSWORD=postgres ENV DB_NAME=postgres ENV REDIS_HOST=redis -# (Optional) Expose ports if your application listens on any (not required for background workers) -# EXPOSE 80 - -# Define the entry point for the container to run the application -ENTRYPOINT ["dotnet", "Worker.dll"] \ No newline at end of file +# Define the entry point +ENTRYPOINT ["dotnet", "Worker.dll"] diff --git a/worker/Worker.csproj b/worker/Worker.csproj index cd843f8247..a7bce45ca5 100644 --- a/worker/Worker.csproj +++ b/worker/Worker.csproj @@ -1,14 +1,16 @@ - net7.0 - linux-arm64 + Exe + net8.0 + enable + enable - - + + From e0ff93f56f771d8ba899527f87bff86fc508e09b Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Sat, 28 Dec 2024 20:36:12 +0100 Subject: [PATCH 17/21] updated terraform - backend+db moved to private subnet and sg, added ssh permission for public sg. --- terraform/main.tf | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/terraform/main.tf b/terraform/main.tf index 72d9ac837a..bb8ba5d1af 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -39,10 +39,10 @@ resource "aws_instance" "backend" { ami = var.ami_image instance_type = var.instance_type key_name = "voting-app-key" - availability_zone = "us-east-1a" - vpc_security_group_ids = [aws_security_group.public_security_group.id] - subnet_id = aws_subnet.public_subnet.id - associate_public_ip_address = true + availability_zone = "us-east-1b" + vpc_security_group_ids = [aws_security_group.private_security_group.id] + subnet_id = aws_subnet.private_subnet.id + associate_public_ip_address = false tags = { Name = "backend" } @@ -52,10 +52,10 @@ resource "aws_instance" "db" { ami = var.ami_image instance_type = var.instance_type key_name = "voting-app-key" - availability_zone = "us-east-1a" - vpc_security_group_ids = [aws_security_group.public_security_group.id] - subnet_id = aws_subnet.public_subnet.id - associate_public_ip_address = true + availability_zone = "us-east-1b" + vpc_security_group_ids = [aws_security_group.private_security_group.id] + subnet_id = aws_subnet.private_subnet.id + associate_public_ip_address = false tags = { Name = "db" } @@ -150,12 +150,14 @@ resource "aws_security_group" "private_security_group" { description = "Allow private traffic " vpc_id = aws_vpc.voting-app-vpc.id + # Allow SSH from the frontend EC2 instance to backend and db instances ingress { - description = "TLS from Internet" - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = [aws_subnet.public_subnet.cidr_block] + description = "SSH from Frontend EC2" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [] # Keep empty if using specific security group as source + security_groups = [aws_security_group.public_security_group.id] # Restrict based on the public security group of frontend } egress { From 12986248388fbb7d1cb02a82871aab5f67c1b4ca Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Sun, 29 Dec 2024 13:40:01 +0100 Subject: [PATCH 18/21] updated ansible configurations (frontend acts as bastion);(still needs debugging) --- ansible/inventory.ini | 6 ++--- ansible/inventory.yml | 19 +++++++++++++ ansible/main.yml | 63 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 ansible/inventory.yml diff --git a/ansible/inventory.ini b/ansible/inventory.ini index d81a05989f..58b2d05f5c 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -1,8 +1,8 @@ [frontend] -54.147.170.240 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu +54.167.3.82 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem # Path to the key pair on your local machine [backend] -44.223.35.102 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu +10.0.2.150 ansible_user=ubuntu ansible_ssh_private_key_file=home/ubuntu/voting-app-key.pem # Path to the key pair on the "frontend" EC2 instance [db] -3.91.95.245 ansible_ssh_private_key_file=../voting-app-key.pem ansible_user=ubuntu +10.0.2.174 ansible_user=ubuntu ansible_ssh_private_key_file=home/ubuntu/voting-app-key.pem # Path to the key pair on the "frontend" EC2 instance diff --git a/ansible/inventory.yml b/ansible/inventory.yml new file mode 100644 index 0000000000..2194525b04 --- /dev/null +++ b/ansible/inventory.yml @@ -0,0 +1,19 @@ +all: + children: + db: + hosts: + db-instance: + ansible_host: 10.0.2.174 + ansible_user: ubuntu + ansible_ssh_private_key_file: /home/ubuntu/voting-app-key.pem # On the frontend EC2 + backend: + hosts: + backend-instance: + ansible_host: 10.0.2.150 + ansible_user: ubuntu + ansible_ssh_private_key_file: /home/ubuntu/voting-app-key.pem + frontend: + hosts: + localhost: + ansible_connection: local + diff --git a/ansible/main.yml b/ansible/main.yml index e5c3e82043..45c9e86c86 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -1,4 +1,61 @@ --- -- import_playbook: db.yml -- import_playbook: backend.yml -- import_playbook: frontend.yml +- name: Prepare frontend as a bastion to deploy backend and db configurations + hosts: frontend + become: yes + tasks: + - name: Add Ansible PPA repository + apt_repository: + repo: "ppa:ansible/ansible" + state: present + + - name: Update apt package index + apt: + update_cache: yes + + # Install Ansible on the frontend instance + - name: Install Ansible + apt: + name: ansible + state: present + + # Transfer private key for SSH access + - name: Copy private key to the frontend instance + copy: + src: ../voting-app-key.pem # Path to the key pair from your local machine + dest: /home/ubuntu/voting-app-key.pem + owner: ubuntu + group: ubuntu + mode: '0600' + + # This ensures that the destination directory is empty/clean (otherwise it will give an error) + - name: Ensure destination directory is clean + file: + path: /home/ubuntu/ansible-config + state: absent + + # Copy Ansible playbooks and configurations + - name: Transfer playbooks and inventory to frontend instance + copy: + src: /Users/ghenadie/Desktop/voting_app_solution/example-voting-app/ansible/ # Path to the Ansible configurations on your local machine (yes, you need the full path here) + dest: /home/ubuntu/ansible-config/ + owner: ubuntu + group: ubuntu + mode: '0755' + + # Run Ansible playbooks for db + - name: Run db configuration from the frontend + shell: | + cd /home/ubuntu/ansible-config && + ansible-playbook -i /home/ubuntu/ansible-config/inventory.ini db.yml + + # Run Ansible playbooks for backend + - name: Run backend configuration from the frontend + shell: | + cd /home/ubuntu/ansible-config && + ansible-playbook -i /home/ubuntu/ansible-config/inventory.ini backend.yml + + # Run frontend configurations on the frontend instance + - name: Run frontend configuration playbook on itself + shell: | + cd /home/ubuntu/ansible-config && + ansible-playbook -i /home/ubuntu/ansible-config/inventory.ini frontend.yml From 4399b847ef1f9af0cec8a76268e0b28e4137ea41 Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Mon, 30 Dec 2024 16:28:08 +0100 Subject: [PATCH 19/21] updates on ansible playbooks + small changes to terraform --- ansible/backend.yml | 2 +- ansible/frontend.yml | 8 ++-- ansible/inventory.ini | 22 +++++++++-- ansible/main.yml | 89 +++++++++++++++++++++++++++++++++++++------ terraform/main.tf | 39 +++++++++++++------ 5 files changed, 130 insertions(+), 30 deletions(-) diff --git a/ansible/backend.yml b/ansible/backend.yml index b542e885ee..e7d82bf0e4 100644 --- a/ansible/backend.yml +++ b/ansible/backend.yml @@ -73,7 +73,7 @@ env: REDIS_HOST: "Redis" # This is how the worker will connect to Redis container by using the container name as hostname REDIS_PORT: "6379" - DB_HOST: "3.91.95.245" + DB_HOST: "10.0.2.174" DB_USERNAME: "postgres" DB_PASSWORD: "postgres" DB_NAME: "voting-app-db" diff --git a/ansible/frontend.yml b/ansible/frontend.yml index d556fc5d55..85f9ca80df 100644 --- a/ansible/frontend.yml +++ b/ansible/frontend.yml @@ -1,6 +1,6 @@ --- - name: Ensure Docker is installed and running on the frontend instance - hosts: frontend + hosts: localhost become: yes tasks: - name: Update apt package index @@ -40,7 +40,7 @@ state: started restart_policy: always env: - REDIS_HOST: "44.223.35.102" # This is how the worker will connect to Redis container by using the container name as hostname + REDIS_HOST: "10.0.2.150" # This is how the worker will connect to Redis container by using the container name as hostname REDIS_PORT: "6379" exposed_ports: - "80" @@ -55,7 +55,7 @@ ##### Result Service ##### - name: Ensure Docker is installed and running on the vote instance - hosts: frontend + hosts: localhost become: yes tasks: - name: Update apt package index @@ -90,7 +90,7 @@ published_ports: - "8081:80" env: - PG_HOST: "3.91.95.245" + PG_HOST: "10.0.2.174" PG_PORT: "5432" PG_USER: "postgres" PG_PASSWORD: "postgres" diff --git a/ansible/inventory.ini b/ansible/inventory.ini index 58b2d05f5c..a94736a943 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -1,8 +1,24 @@ +; [frontend] +; 54.167.3.82 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem # Path to the key pair on your local machine + +; [backend] +; 10.0.2.150 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem # Path to the key pair on the "frontend" EC2 instance + +; [db] +; 10.0.2.174 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem # Path to the key pair on the "frontend" EC2 instance + + [frontend] -54.167.3.82 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem # Path to the key pair on your local machine +54.167.3.82 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem [backend] -10.0.2.150 ansible_user=ubuntu ansible_ssh_private_key_file=home/ubuntu/voting-app-key.pem # Path to the key pair on the "frontend" EC2 instance +10.0.2.150 ansible_user=ubuntu ansible_ssh_private_key_file=/home/ubuntu/voting-app-key.pem \ [db] -10.0.2.174 ansible_user=ubuntu ansible_ssh_private_key_file=home/ubuntu/voting-app-key.pem # Path to the key pair on the "frontend" EC2 instance +10.0.2.174 ansible_user=ubuntu ansible_ssh_private_key_file=/home/ubuntu/voting-app-key.pem \ + +[db:vars] +ansible_ssh_common_args='-o ProxyCommand="ssh -i ../voting-app-key.pem -W %h:22 ubuntu@54.167.3.82" -o ConnectTimeout=3000' + +[backend:vars] +ansible_ssh_common_args='-o ProxyCommand="ssh -i ../voting-app-key.pem -W %h:22 ubuntu@54.167.3.82" -o ConnectTimeout=3000' \ No newline at end of file diff --git a/ansible/main.yml b/ansible/main.yml index 45c9e86c86..5b2ae9b71d 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -1,4 +1,5 @@ --- +#####--------------------------------------------------- Play 1: Prepare frontend as a bastion ---------------------------------------------------##### - name: Prepare frontend as a bastion to deploy backend and db configurations hosts: frontend become: yes @@ -34,7 +35,7 @@ state: absent # Copy Ansible playbooks and configurations - - name: Transfer playbooks and inventory to frontend instance + - name: Transfer Ansible playbooks to frontend instance copy: src: /Users/ghenadie/Desktop/voting_app_solution/example-voting-app/ansible/ # Path to the Ansible configurations on your local machine (yes, you need the full path here) dest: /home/ubuntu/ansible-config/ @@ -42,20 +43,86 @@ group: ubuntu mode: '0755' - # Run Ansible playbooks for db - - name: Run db configuration from the frontend +#####--------------------------------------------------- Play 2: Test SSH connection from "bastion" to "db" and "backend" ---------------------------------------------------##### +# Test the SSH to "DB" +# - name: Test SSH connection through bastion to db instance +# hosts: frontend +# tasks: + - name: Ping db from the frontend instance + shell: ssh -i "voting-app-key.pem" -o ProxyCommand="ssh -i 'voting-app-key.pem' -W %h:%p ubuntu@54.167.3.82" ubuntu@10.0.2.174 echo "Connection successful" + register: test_ssh + changed_when: false + + - name: Output SSH test results + debug: + var: test_ssh.stdout + +# Test the SSH to "backend" +# - name: Test SSH connection through bastion to backend instance +# hosts: frontend +# tasks: + - name: Ping backend from the frontend instance + shell: ssh -i "voting-app-key.pem" -o ProxyCommand="ssh -i 'voting-app-key.pem' -W %h:%p ubuntu@54.167.3.82" ubuntu@10.0.2.150 echo "Connection successful" + register: test_ssh + changed_when: false + + - name: Output SSH test results + debug: + var: test_ssh.stdout + +#####--------------------------------------------------- Play 3: Copy PEM key to "db" and "backend" instances ---------------------------------------------------##### +# - name: Copy PEM key to db EC2 instance +# hosts: frontend +# tasks: +# - name: Copy PEM key to db instance +# ansible.builtin.copy: +# src: /home/ubuntu/voting-app-key.pem +# dest: /home/ubuntu/.ssh/voting-app-key.pem +# mode: '0400' # Ensure secure permissions +# owner: ubuntu +# group: ubuntu + +# - name: Copy PEM key to backend EC2 instance +# hosts: frontend +# tasks: +# - name: Copy PEM key to backend instance +# ansible.builtin.copy: +# src: /home/ubuntu/voting-app-key.pem +# dest: /home/ubuntu/.ssh/voting-app-key.pem +# mode: '0400' # Ensure secure permissions +# owner: ubuntu +# group: ubuntu + + + +#####--------------------------------------------------- Play 4: Run "db" and "backend" configuration playbooks ---------------------------------------------------##### +# DB +# - name: Run db configurations on db EC2 +# hosts: db # change to DB (delete hosts in db.yml) +# become: yes +# tasks: + - name: Run db configuration shell: | cd /home/ubuntu/ansible-config && - ansible-playbook -i /home/ubuntu/ansible-config/inventory.ini db.yml - - # Run Ansible playbooks for backend - - name: Run backend configuration from the frontend + ansible-playbook -i inventory.ini db.yml +# backend +# - name: Run backend configuration on backend EC2 +# hosts: backend # change to backend (delete hosts in backend.yml) +# become: yes +# tasks: + - name: Run backend configuration shell: | cd /home/ubuntu/ansible-config && - ansible-playbook -i /home/ubuntu/ansible-config/inventory.ini backend.yml + ansible-playbook -i inventory.ini backend.yml - # Run frontend configurations on the frontend instance - - name: Run frontend configuration playbook on itself +#####--------------------------------------------------- Play 5: Run "frontend" configuration playbooks ---------------------------------------------------##### +# Run frontend configurations on the frontend instance +# - name: Run frontend configuration on frontend EC2 +# hosts: frontend +# become: yes +# tasks: + - name: Run frontend playbook shell: | cd /home/ubuntu/ansible-config && - ansible-playbook -i /home/ubuntu/ansible-config/inventory.ini frontend.yml + ansible-playbook -i inventory.ini frontend.yml + diff --git a/terraform/main.tf b/terraform/main.tf index bb8ba5d1af..90b6a434db 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -131,10 +131,7 @@ resource "aws_security_group" "public_security_group" { protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } - tags = { - Name = "voting-app_public-sec-gr" - } - + ingress { description = "PostgreSQL from anywhere" from_port = 5432 @@ -142,6 +139,18 @@ resource "aws_security_group" "public_security_group" { protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } + + egress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [] + security_groups = [aws_security_group.private_security_group.id] + } + + tags = { + Name = "voting-app_public-sec-gr" + } } # Private # @@ -152,13 +161,21 @@ resource "aws_security_group" "private_security_group" { # Allow SSH from the frontend EC2 instance to backend and db instances ingress { - description = "SSH from Frontend EC2" - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = [] # Keep empty if using specific security group as source - security_groups = [aws_security_group.public_security_group.id] # Restrict based on the public security group of frontend - } + description = "Allow SSH from Frontend EC2" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [] + security_groups = [aws_security_group.public_security_group.id] +} + + ingress { + description = "Allow communication with backend and DB instances" + from_port = 0 + to_port = 65535 + protocol = "tcp" + security_groups = [aws_security_group.public_security_group.id] +} egress { from_port = 0 From 638a248bc7eb0a4e51f687718096350691076620 Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Tue, 31 Dec 2024 19:02:49 +0100 Subject: [PATCH 20/21] updated ansible playbooks --- ansible/backend.yml | 121 +++++++++++++++++++++++++++++++++++++----- ansible/db.yml | 60 ++++++++++++++++++--- ansible/frontend.yml | 75 +++++++++++++++----------- ansible/inventory.ini | 21 +++----- ansible/inventory.yml | 19 ------- ansible/main.yml | 22 ++++---- 6 files changed, 224 insertions(+), 94 deletions(-) delete mode 100644 ansible/inventory.yml diff --git a/ansible/backend.yml b/ansible/backend.yml index e7d82bf0e4..83e9b14fcf 100644 --- a/ansible/backend.yml +++ b/ansible/backend.yml @@ -1,20 +1,55 @@ --- -- name: Ensure Docker is installed and running on the backend instance +#####--------------------------------------------------- Play 1: Docker Installation ---------------------------------------------------##### + +- name: Ensure Docker is installed and running on the 'backend' instance hosts: backend become: yes + vars: + postgres_password: "postgres" + postgres_db: "voting_app_db" + postgres_user: "voting_user" + postgres_port: 5432 tasks: - name: Update apt package index ansible.builtin.apt: update_cache: yes - - name: Install Docker if not installed + - name: Remove conflicting containerd packages (if any) + ansible.builtin.apt: + name: containerd + state: absent + + - name: Install Docker dependencies ansible.builtin.apt: - name: docker.io + name: + - apt-transport-https + - ca-certificates + - curl + - software-properties-common state: present - - name: Create backend Docker network if it does not exist - ansible.builtin.docker_network: - name: backend-network + - name: Add Docker's GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: /usr/share/keyrings/docker-archive-keyring.gpg + notify: Update apt-key + + - name: Add Docker repository + ansible.builtin.apt_repository: + repo: "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + + + - name: Update apt package index (after adding Docker repository) + ansible.builtin.apt: + update_cache: yes + + - name: Install Docker CE + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io state: present - name: Add the user to the Docker group @@ -24,19 +59,78 @@ append: yes state: present - - name: Ensure Docker service is running + - name: Ensure Docker service is running and enabled ansible.builtin.service: name: docker state: started enabled: true + # - name: Ensure Docker network "backend-network" exists + # ansible.builtin.command: + # cmd: docker network create backend-network + # register: docker_network_creation + # failed_when: docker_network_creation.stderr != "" + # ignore_errors: true + + - name: Ensure Docker network "backend-network" exists + docker_network: + name: backend-network + state: present + ignore_errors: true + +#####--------------------------------------------------- Play 2: Running "Redis" and "Worker" Containers ---------------------------------------------------##### + +- name: Running "Redis" and "Worker" Containers + hosts: backend + become: yes + tasks: ##### Redis Service ##### - - name: Pull the Redis Docker image + - name: Pull the 'Redis' Docker image ansible.builtin.docker_image: name: redis:alpine source: pull + + - name: Check if port 6379 is in use + shell: | + lsof -i :6379 | grep -v COMMAND | awk '{print $2}' || echo "" + register: port_check + changed_when: port_check.stdout != "" + ignore_errors: true + + # - name: Kill process using port 6379 (if any) + # shell: | + # kill -9 {{ port_check.stdout }} + # when: port_check.stdout != "" + # ignore_errors: true + + - name: Kill process using port 6379 (if any) + shell: | + PID=$(lsof -ti :6379) + if [ -n "$PID" ]; then + kill -9 $PID + fi + register: kill_output + failed_when: false + changed_when: kill_output.stdout != "" - - name: Run the Redis container + - name: Stop all running containers that use port 6379 + docker_container: + name: "{{ item }}" + state: stopped + loop: "{{ lookup('docker_container', '', True).splitlines() }}" + when: item.ports is defined and '6379/tcp' in item.ports + failed_when: false + ignore_errors: true + + - name: Wait for port 6379 to be free + wait_for: + host: 127.0.0.1 + port: 6379 + state: stopped + timeout: 10 + when: port_check.stdout != "" + + - name: Run the 'Redis' container ansible.builtin.docker_container: name: Redis image: redis:alpine @@ -48,21 +142,22 @@ - "6379" published_ports: - "6379:6379" + when: port_check.stdout == "" + failed_when: false - - name: Verify the Redis container is running + - name: Verify the 'Redis' container is running ansible.builtin.docker_container_info: name: Redis register: redis_container_info - ##### Worker Service ##### - - name: Pull the worker Docker image + - name: Pull the 'worker' Docker image ansible.builtin.docker_image: name: ghenac/voting-app-worker tag: latest source: pull - - name: Run the worker-app container + - name: Run the 'worker' container ansible.builtin.docker_container: name: voting-app-worker image: ghenac/voting-app-worker:latest diff --git a/ansible/db.yml b/ansible/db.yml index 140c2af267..c11fa53d09 100644 --- a/ansible/db.yml +++ b/ansible/db.yml @@ -1,5 +1,7 @@ --- -- name: Ensure Docker is installed and running on the DB instance +#####--------------------------------------------------- Play 1: Docker Installation ---------------------------------------------------##### + +- name: Ensure Docker is installed and running on the 'DB' instance hosts: db become: yes vars: @@ -12,9 +14,42 @@ ansible.builtin.apt: update_cache: yes - - name: Install Docker if not installed + - name: Remove conflicting containerd packages (if any) + ansible.builtin.apt: + name: containerd + state: absent + + - name: Install Docker dependencies + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - software-properties-common + state: present + + - name: Add Docker's GPG key + ansible.builtin.get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: /usr/share/keyrings/docker-archive-keyring.gpg + notify: Update apt-key + + - name: Add Docker repository + ansible.builtin.apt_repository: + repo: "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + + + - name: Update apt package index (after adding Docker repository) + ansible.builtin.apt: + update_cache: yes + + - name: Install Docker CE ansible.builtin.apt: - name: docker.io + name: + - docker-ce + - docker-ce-cli + - containerd.io state: present - name: Add the user to the Docker group @@ -30,13 +65,26 @@ state: started enabled: true - - name: Pull the PostgreSQL Docker image + - name: Ensure Docker network "voting_app_network" exists + ansible.builtin.command: + cmd: docker network create voting_app_network + register: docker_network_creation + failed_when: docker_network_creation.stderr != "" + ignore_errors: true + +#####--------------------------------------------------- Play 2: Running "DB" Container ---------------------------------------------------##### + +- name: Running "DB" Container + hosts: db + become: yes + tasks: + ##### DB service ##### + - name: Pull the 'PostgreSQL' Docker image community.docker.docker_image: name: postgres:15-alpine source: pull - ##### DB service ##### - - name: Run the PostgreSQL container + - name: Run the 'PostgreSQL' container community.docker.docker_container: name: voting-app-db image: postgres:15-alpine diff --git a/ansible/frontend.yml b/ansible/frontend.yml index 85f9ca80df..c1f73a6d9c 100644 --- a/ansible/frontend.yml +++ b/ansible/frontend.yml @@ -1,16 +1,40 @@ --- -- name: Ensure Docker is installed and running on the frontend instance +#####--------------------------------------------------- Play 1: Docker Installation ---------------------------------------------------##### + +- name: Ensure Docker is installed and running on the 'frontend' instance hosts: localhost become: yes + vars: + postgres_password: "postgres" + postgres_db: "voting_app_db" + postgres_user: "voting_user" + postgres_port: 5432 tasks: - name: Update apt package index ansible.builtin.apt: update_cache: yes - - name: Install Docker if not installed - ansible.builtin.apt: + # - name: Remove conflicting containerd packages (if any) + # ansible.builtin.apt: + # name: containerd + # state: absent + + - name: Check if Docker is installed + command: docker --version + register: docker_installed + ignore_errors: true + + - name: Install Docker if not present + package: name: docker.io state: present + when: docker_installed.rc != 0 + + - name: Ensure Docker is installed + command: docker --version + register: docker_check + failed_when: docker_check.rc != 0 + changed_when: false - name: Add the user to the Docker group ansible.builtin.user: @@ -19,15 +43,27 @@ append: yes state: present - - name: Ensure Docker service is running + - name: Ensure Docker service is running and enabled ansible.builtin.service: name: docker state: started - enabled: true + enabled: yes + + - name: Ensure Docker network "frontend-network" exists + ansible.builtin.command: + cmd: docker network create frontend-network + register: docker_network_creation + failed_when: docker_network_creation.stderr != "" + ignore_errors: true -##### Vote Service ##### +#####--------------------------------------------------- Play 2: Running "vote" and "result" containers ---------------------------------------------------##### - - name: Pull the vote Docker image +- name: Running "vote" and "result" containers + hosts: localhost + become: yes + tasks: + ##### Vote Service ##### + - name: Pull the 'vote' Docker image ansible.builtin.docker_image: name: ghenac/voting-app-vote tag: latest @@ -52,28 +88,8 @@ name: voting-app-vote register: container_info - -##### Result Service ##### -- name: Ensure Docker is installed and running on the vote instance - hosts: localhost - become: yes - tasks: - - name: Update apt package index - ansible.builtin.apt: - update_cache: yes - - - name: Install Docker if not installed - ansible.builtin.apt: - name: docker.io - state: present - - - name: Ensure Docker service is running - ansible.builtin.service: - name: docker - state: started - enabled: true - - - name: Pull the result Docker image + ##### Result Service ##### + - name: Pull the 'result' Docker image ansible.builtin.docker_image: name: ghenac/voting-app-result tag: latest @@ -96,7 +112,6 @@ PG_PASSWORD: "postgres" PG_DATABASE: "voting-app-db" - - name: Verify the container is running ansible.builtin.docker_container_info: name: voting-app-result diff --git a/ansible/inventory.ini b/ansible/inventory.ini index a94736a943..cb49c532b6 100644 --- a/ansible/inventory.ini +++ b/ansible/inventory.ini @@ -1,24 +1,15 @@ -; [frontend] -; 54.167.3.82 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem # Path to the key pair on your local machine - -; [backend] -; 10.0.2.150 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem # Path to the key pair on the "frontend" EC2 instance - -; [db] -; 10.0.2.174 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem # Path to the key pair on the "frontend" EC2 instance - - [frontend] -54.167.3.82 ansible_user=ubuntu ansible_ssh_private_key_file=../voting-app-key.pem +54.167.3.82 ansible_host=54.167.3.82 ansible_user=ubuntu ansible_ssh_private_key_file=/users/ghenadie/desktop/voting_app_solution/example-voting-app/voting-app-key.pem [backend] -10.0.2.150 ansible_user=ubuntu ansible_ssh_private_key_file=/home/ubuntu/voting-app-key.pem \ +10.0.2.150 ansible_host=10.0.2.150 ansible_user=ubuntu ansible_ssh_private_key_file=/home/ubuntu/voting-app-key.pem [db] -10.0.2.174 ansible_user=ubuntu ansible_ssh_private_key_file=/home/ubuntu/voting-app-key.pem \ +10.0.2.174 ansible_host=10.0.2.174 ansible_user=ubuntu ansible_ssh_private_key_file=/home/ubuntu/voting-app-key.pem + [db:vars] -ansible_ssh_common_args='-o ProxyCommand="ssh -i ../voting-app-key.pem -W %h:22 ubuntu@54.167.3.82" -o ConnectTimeout=3000' +ansible_ssh_common_args='-o ProxyCommand="ssh -W %h:%p -i /home/ubuntu/voting-app-key.pem ubuntu@54.167.3.82"' [backend:vars] -ansible_ssh_common_args='-o ProxyCommand="ssh -i ../voting-app-key.pem -W %h:22 ubuntu@54.167.3.82" -o ConnectTimeout=3000' \ No newline at end of file +ansible_ssh_common_args='-o ProxyCommand="ssh -W %h:%p -i /home/ubuntu/voting-app-key.pem ubuntu@54.167.3.82"' diff --git a/ansible/inventory.yml b/ansible/inventory.yml deleted file mode 100644 index 2194525b04..0000000000 --- a/ansible/inventory.yml +++ /dev/null @@ -1,19 +0,0 @@ -all: - children: - db: - hosts: - db-instance: - ansible_host: 10.0.2.174 - ansible_user: ubuntu - ansible_ssh_private_key_file: /home/ubuntu/voting-app-key.pem # On the frontend EC2 - backend: - hosts: - backend-instance: - ansible_host: 10.0.2.150 - ansible_user: ubuntu - ansible_ssh_private_key_file: /home/ubuntu/voting-app-key.pem - frontend: - hosts: - localhost: - ansible_connection: local - diff --git a/ansible/main.yml b/ansible/main.yml index 5b2ae9b71d..7c8ff5cd55 100644 --- a/ansible/main.yml +++ b/ansible/main.yml @@ -26,7 +26,7 @@ dest: /home/ubuntu/voting-app-key.pem owner: ubuntu group: ubuntu - mode: '0600' + mode: '0400' # This ensures that the destination directory is empty/clean (otherwise it will give an error) - name: Ensure destination directory is clean @@ -45,9 +45,9 @@ #####--------------------------------------------------- Play 2: Test SSH connection from "bastion" to "db" and "backend" ---------------------------------------------------##### # Test the SSH to "DB" -# - name: Test SSH connection through bastion to db instance -# hosts: frontend -# tasks: +- name: Test SSH connection through bastion to db instance + hosts: frontend + tasks: - name: Ping db from the frontend instance shell: ssh -i "voting-app-key.pem" -o ProxyCommand="ssh -i 'voting-app-key.pem' -W %h:%p ubuntu@54.167.3.82" ubuntu@10.0.2.174 echo "Connection successful" register: test_ssh @@ -58,9 +58,9 @@ var: test_ssh.stdout # Test the SSH to "backend" -# - name: Test SSH connection through bastion to backend instance -# hosts: frontend -# tasks: +- name: Test SSH connection through bastion to backend instance + hosts: frontend + tasks: - name: Ping backend from the frontend instance shell: ssh -i "voting-app-key.pem" -o ProxyCommand="ssh -i 'voting-app-key.pem' -W %h:%p ubuntu@54.167.3.82" ubuntu@10.0.2.150 echo "Connection successful" register: test_ssh @@ -95,13 +95,13 @@ -#####--------------------------------------------------- Play 4: Run "db" and "backend" configuration playbooks ---------------------------------------------------##### +#####--------------------------------------------------- Play 4: Run "db" and "backend" configuration playbook ---------------------------------------------------##### # DB # - name: Run db configurations on db EC2 # hosts: db # change to DB (delete hosts in db.yml) # become: yes # tasks: - - name: Run db configuration + - name: Run db playbook shell: | cd /home/ubuntu/ansible-config && ansible-playbook -i inventory.ini db.yml @@ -110,12 +110,12 @@ # hosts: backend # change to backend (delete hosts in backend.yml) # become: yes # tasks: - - name: Run backend configuration + - name: Run backend playbook shell: | cd /home/ubuntu/ansible-config && ansible-playbook -i inventory.ini backend.yml -#####--------------------------------------------------- Play 5: Run "frontend" configuration playbooks ---------------------------------------------------##### +#####--------------------------------------------------- Play 5: Run "frontend" configuration playbook ---------------------------------------------------##### # Run frontend configurations on the frontend instance # - name: Run frontend configuration on frontend EC2 # hosts: frontend From f141cd5342dd89283d139f8800b0e63609533075 Mon Sep 17 00:00:00 2001 From: ghenadie_cadin Date: Fri, 3 Jan 2025 15:07:56 +0100 Subject: [PATCH 21/21] The final Ansible coonfigurations update. App works on AWS infrastructure --- .DS_Store | Bin 0 -> 6148 bytes ansible/backend.yml | 67 +++++------------------------------------- ansible/db.yml | 51 ++++---------------------------- ansible/frontend.yml | 9 +++--- ansible/inventory.ini | 6 ++-- ansible/main.yml | 50 ++++++------------------------- 6 files changed, 31 insertions(+), 152 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..64ea552e9d4f3fc02bde8f238215282e6ec0acb3 GIT binary patch literal 6148 zcmeHKL2nX46n;Yy7ORFdYU;sc6R!oSHZ_eGO5xB%Jrtt{HOR8GC0%yNE)*L=!dd@= zf5EH2#Q)++zc(|OUBGxFYUYud@6F76^WJ>BJHrr>sEzzxq9PGFD2&A3|IY7UPA>K+HO;{{9Bw!<=D8Ti{6;BPliZ3-!-9wokC>0yMog+r9`4<*o+ti5qO z@B(H1)f)u8cvz`?6Pbd$K>>aVH0lD$pPA zPp1Xv&Vz?fPg)m2Ka?L;-XyR`DYSP#7yQ02_~q#t9n%pt=mj;Yk+#k- zf5|8XI)UXO)#*8~jLiMChkDySC*HOk_-0l|oY{gcbDFd*Aa{N~5!P`ECL-#mJrsjU zkE~(LoLTdyrPxW@ZLMUsq~3=stYd(l(dEG8pM1L&gL3o z*$|ci%fP?I0Phbj3S-saOrzR5P^l{bFpF*_(D_dT`?v%YbFz zzhXdS>&