├── .gitignore ├── LICENSE ├── README.md ├── azure-vote-all-in-one-redis.yaml ├── azure-vote ├── Dockerfile ├── Dockerfile-for-app-service ├── app_init.supervisord.conf ├── azure-vote │ ├── config_file.cfg │ ├── main.py │ ├── static │ │ └── default.css │ └── templates │ │ └── index.html └── sshd_config ├── docker-compose.yaml └── jenkins-tutorial ├── config-jenkins.sh └── deploy-jenkins-vm.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - python 5 | products: 6 | - azure 7 | - azure-redis-cache 8 | description: "This sample creates a multi-container application in an Azure Kubernetes Service (AKS) cluster." 9 | --- 10 | 11 | # Azure Voting App 12 | 13 | This sample creates a multi-container application in an Azure Kubernetes Service (AKS) cluster. The application interface has been built using Python / Flask. The data component is using Redis. 14 | 15 | To walk through a quick deployment of this application, see the AKS [quick start](https://docs.microsoft.com/en-us/azure/aks/kubernetes-walkthrough?WT.mc_id=none-github-nepeters). 16 | 17 | To walk through a complete experience where this code is packaged into container images, uploaded to Azure Container Registry, and then run in and AKS cluster, see the [AKS tutorials](https://docs.microsoft.com/en-us/azure/aks/tutorial-kubernetes-prepare-app?WT.mc_id=none-github-nepeters). 18 | 19 | ## Contributing 20 | 21 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 22 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 23 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 24 | 25 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 26 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 27 | provided by the bot. You will only need to do this once across all repos using our CLA. 28 | 29 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 30 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 31 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 32 | -------------------------------------------------------------------------------- /azure-vote-all-in-one-redis.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: azure-vote-back 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | app: azure-vote-back 10 | template: 11 | metadata: 12 | labels: 13 | app: azure-vote-back 14 | spec: 15 | nodeSelector: 16 | "kubernetes.io/os": linux 17 | containers: 18 | - name: azure-vote-back 19 | image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 20 | env: 21 | - name: ALLOW_EMPTY_PASSWORD 22 | value: "yes" 23 | ports: 24 | - containerPort: 6379 25 | name: redis 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: azure-vote-back 31 | spec: 32 | ports: 33 | - port: 6379 34 | selector: 35 | app: azure-vote-back 36 | --- 37 | apiVersion: apps/v1 38 | kind: Deployment 39 | metadata: 40 | name: azure-vote-front 41 | spec: 42 | replicas: 1 43 | selector: 44 | matchLabels: 45 | app: azure-vote-front 46 | strategy: 47 | rollingUpdate: 48 | maxSurge: 1 49 | maxUnavailable: 1 50 | minReadySeconds: 5 51 | template: 52 | metadata: 53 | labels: 54 | app: azure-vote-front 55 | spec: 56 | nodeSelector: 57 | "kubernetes.io/os": linux 58 | containers: 59 | - name: azure-vote-front 60 | image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 61 | ports: 62 | - containerPort: 80 63 | resources: 64 | requests: 65 | cpu: 250m 66 | limits: 67 | cpu: 500m 68 | env: 69 | - name: REDIS 70 | value: "azure-vote-back" 71 | --- 72 | apiVersion: v1 73 | kind: Service 74 | metadata: 75 | name: azure-vote-front 76 | spec: 77 | type: LoadBalancer 78 | ports: 79 | - port: 80 80 | selector: 81 | app: azure-vote-front 82 | -------------------------------------------------------------------------------- /azure-vote/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uwsgi-nginx-flask:python3.6 2 | RUN pip install redis 3 | ADD /azure-vote /app 4 | -------------------------------------------------------------------------------- /azure-vote/Dockerfile-for-app-service: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uwsgi-nginx-flask:python3.6 2 | 3 | COPY sshd_config /etc/ssh/ 4 | COPY app_init.supervisord.conf /etc/supervisor/conf.d 5 | 6 | RUN mkdir -p /home/LogFiles \ 7 | && echo "root:Docker!" | chpasswd \ 8 | && echo "cd /home" >> /etc/bash.bashrc \ 9 | && apt update \ 10 | && apt install -y --no-install-recommends openssh-server vim curl wget tcptraceroute 11 | 12 | RUN pip install redis 13 | 14 | EXPOSE 2222 80 15 | 16 | ADD /azure-vote /app 17 | 18 | ENV PORT 80 19 | ENV PATH ${PATH}:/home/site/wwwroot 20 | 21 | # Supervisor will call into /opt/startup/init_container.sh 22 | # Also see: http://blog.trifork.com/2014/03/11/using-supervisor-with-docker-to-manage-processes-supporting-image-inheritance/ 23 | CMD ["/usr/bin/supervisord"] 24 | -------------------------------------------------------------------------------- /azure-vote/app_init.supervisord.conf: -------------------------------------------------------------------------------- 1 | [program:sshd] 2 | command=service ssh start 3 | stdout_logfile=/var/log/supervisor/%(program_name)s.log 4 | stderr_logfile=/var/log/supervisor/%(program_name)s.log 5 | user=root -------------------------------------------------------------------------------- /azure-vote/azure-vote/config_file.cfg: -------------------------------------------------------------------------------- 1 | # UI Configurations 2 | TITLE = 'Azure Voting App' 3 | VOTE1VALUE = 'Cats' 4 | VOTE2VALUE = 'Dogs' 5 | SHOWHOST = 'false' -------------------------------------------------------------------------------- /azure-vote/azure-vote/main.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, render_template 2 | import os 3 | import random 4 | import redis 5 | import socket 6 | import sys 7 | 8 | app = Flask(__name__) 9 | 10 | # Load configurations from environment or config file 11 | app.config.from_pyfile('config_file.cfg') 12 | 13 | if ("VOTE1VALUE" in os.environ and os.environ['VOTE1VALUE']): 14 | button1 = os.environ['VOTE1VALUE'] 15 | else: 16 | button1 = app.config['VOTE1VALUE'] 17 | 18 | if ("VOTE2VALUE" in os.environ and os.environ['VOTE2VALUE']): 19 | button2 = os.environ['VOTE2VALUE'] 20 | else: 21 | button2 = app.config['VOTE2VALUE'] 22 | 23 | if ("TITLE" in os.environ and os.environ['TITLE']): 24 | title = os.environ['TITLE'] 25 | else: 26 | title = app.config['TITLE'] 27 | 28 | # Redis configurations 29 | redis_server = os.environ['REDIS'] 30 | 31 | # Redis Connection 32 | try: 33 | if "REDIS_PWD" in os.environ: 34 | r = redis.StrictRedis(host=redis_server, 35 | port=6379, 36 | password=os.environ['REDIS_PWD']) 37 | else: 38 | r = redis.Redis(redis_server) 39 | r.ping() 40 | except redis.ConnectionError: 41 | exit('Failed to connect to Redis, terminating.') 42 | 43 | # Change title to host name to demo NLB 44 | if app.config['SHOWHOST'] == "true": 45 | title = socket.gethostname() 46 | 47 | # Init Redis 48 | if not r.get(button1): r.set(button1,0) 49 | if not r.get(button2): r.set(button2,0) 50 | 51 | @app.route('/', methods=['GET', 'POST']) 52 | def index(): 53 | 54 | if request.method == 'GET': 55 | 56 | # Get current values 57 | vote1 = r.get(button1).decode('utf-8') 58 | vote2 = r.get(button2).decode('utf-8') 59 | 60 | # Return index with values 61 | return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) 62 | 63 | elif request.method == 'POST': 64 | 65 | if request.form['vote'] == 'reset': 66 | 67 | # Empty table and return results 68 | r.set(button1,0) 69 | r.set(button2,0) 70 | vote1 = r.get(button1).decode('utf-8') 71 | vote2 = r.get(button2).decode('utf-8') 72 | return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) 73 | 74 | else: 75 | 76 | # Insert vote result into DB 77 | vote = request.form['vote'] 78 | r.incr(vote,1) 79 | 80 | # Get current values 81 | vote1 = r.get(button1).decode('utf-8') 82 | vote2 = r.get(button2).decode('utf-8') 83 | 84 | # Return results 85 | return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) 86 | 87 | if __name__ == "__main__": 88 | app.run() 89 | -------------------------------------------------------------------------------- /azure-vote/azure-vote/static/default.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color:#F8F8F8; 3 | } 4 | 5 | div#container { 6 | margin-top:5%; 7 | } 8 | 9 | div#space { 10 | display:block; 11 | margin: 0 auto; 12 | width: 500px; 13 | height: 10px; 14 | 15 | } 16 | 17 | div#logo { 18 | display:block; 19 | margin: 0 auto; 20 | width: 500px; 21 | text-align: center; 22 | font-size:30px; 23 | font-family:Helvetica; 24 | /*border-bottom: 1px solid black;*/ 25 | } 26 | 27 | div#form { 28 | padding: 20px; 29 | padding-right: 20px; 30 | padding-top: 20px; 31 | display:block; 32 | margin: 0 auto; 33 | width: 500px; 34 | text-align: center; 35 | font-size:30px; 36 | font-family:Helvetica; 37 | border-bottom: 1px solid black; 38 | border-top: 1px solid black; 39 | } 40 | 41 | div#results { 42 | display:block; 43 | margin: 0 auto; 44 | width: 500px; 45 | text-align: center; 46 | font-size:30px; 47 | font-family:Helvetica; 48 | } 49 | 50 | .button { 51 | background-color: #4CAF50; /* Green */ 52 | border: none; 53 | color: white; 54 | padding: 16px 32px; 55 | text-align: center; 56 | text-decoration: none; 57 | display: inline-block; 58 | font-size: 16px; 59 | margin: 4px 2px; 60 | -webkit-transition-duration: 0.4s; /* Safari */ 61 | transition-duration: 0.4s; 62 | cursor: pointer; 63 | width: 250px; 64 | } 65 | 66 | .button1 { 67 | background-color: white; 68 | color: black; 69 | border: 2px solid #008CBA; 70 | } 71 | 72 | .button1:hover { 73 | background-color: #008CBA; 74 | color: white; 75 | } 76 | .button2 { 77 | background-color: white; 78 | color: black; 79 | border: 2px solid #555555; 80 | } 81 | 82 | .button2:hover { 83 | background-color: #555555; 84 | color: white; 85 | } 86 | 87 | .button3 { 88 | background-color: white; 89 | color: black; 90 | border: 2px solid #f44336; 91 | } 92 | 93 | .button3:hover { 94 | background-color: #f44336; 95 | color: white; 96 | } -------------------------------------------------------------------------------- /azure-vote/azure-vote/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{title}} 6 | 7 | 11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 |
{{button1}} - {{ value1 }} | {{button2}} - {{ value2 }}
25 | 26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /azure-vote/sshd_config: -------------------------------------------------------------------------------- 1 | # This is ssh server systemwide configuration file. 2 | # 3 | # /etc/sshd_config 4 | 5 | Port 2222 6 | ListenAddress 0.0.0.0 7 | LoginGraceTime 180 8 | X11Forwarding yes 9 | Ciphers aes128-cbc,3des-cbc,aes256-cbc 10 | MACs hmac-sha1,hmac-sha1-96 11 | StrictModes yes 12 | SyslogFacility DAEMON 13 | PasswordAuthentication yes 14 | PermitEmptyPasswords no 15 | PermitRootLogin yes -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | azure-vote-back: 4 | image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 5 | container_name: azure-vote-back 6 | environment: 7 | ALLOW_EMPTY_PASSWORD: "yes" 8 | ports: 9 | - "6379:6379" 10 | 11 | azure-vote-front: 12 | build: ./azure-vote 13 | image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 14 | container_name: azure-vote-front 15 | environment: 16 | REDIS: azure-vote-back 17 | ports: 18 | - "8080:80" 19 | -------------------------------------------------------------------------------- /jenkins-tutorial/config-jenkins.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Jenkins 4 | wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | sudo apt-key add - 5 | sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list' 6 | sudo add-apt-repository ppa:webupd8team/java -y 7 | sudo apt-get update 8 | echo "oracle-java8-installer shared/accepted-oracle-license-v1-1 select true" | sudo debconf-set-selections 9 | sudo apt-get install oracle-java8-installer -y 10 | sudo apt-get install jenkins -y 11 | 12 | # Docker 13 | sudo apt-get install apt-transport-https ca-certificates curl software-properties-common -y 14 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 15 | sudo apt-key fingerprint 0EBFCD88 16 | sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 17 | sudo apt-get update 18 | sudo apt-get install docker-ce -y 19 | 20 | # Azure CLI 21 | echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ wheezy main" | sudo tee /etc/apt/sources.list.d/azure-cli.list 22 | sudo apt-key adv --keyserver packages.microsoft.com --recv-keys 417A0893 23 | sudo apt-get install apt-transport-https 24 | sudo apt-get update && sudo apt-get install azure-cli 25 | 26 | # Kubectl 27 | cd /tmp/ 28 | sudo curl -kLO https://storage.googleapis.com/kubernetes-release/release/v1.8.0/bin/linux/amd64/kubectl 29 | chmod +x ./kubectl 30 | sudo mv ./kubectl /usr/local/bin/kubectl 31 | 32 | # Configure access 33 | usermod -aG docker jenkins 34 | usermod -aG docker azureuser 35 | sudo touch /var/lib/jenkins/jenkins.install.InstallUtil.lastExecVersion 36 | service jenkins restart 37 | -------------------------------------------------------------------------------- /jenkins-tutorial/deploy-jenkins-vm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export resourceGroup=myResourceGroup$RANDOM 3 | virtualMachine=myVM 4 | adminUser=azureuser 5 | pathToKubeConfig=~/.kube/config 6 | 7 | if [ -f $pathToKubeConfig ] 8 | then 9 | 10 | # Create a resource group. 11 | az group create --name $resourceGroup --location westeurope 12 | 13 | # Create a new virtual machine, this creates SSH keys if not present. 14 | az vm create --resource-group $resourceGroup --name $virtualMachine --admin-username $adminUser --image UbuntuLTS --generate-ssh-keys 15 | 16 | # Open port 80 to allow web traffic to host. 17 | az vm open-port --port 80 --resource-group $resourceGroup --name $virtualMachine --priority 101 18 | 19 | # Open port 22 to allow ssh traffic to host. 20 | az vm open-port --port 22 --resource-group $resourceGroup --name $virtualMachine --priority 102 21 | 22 | # Open port 8080 to allow web traffic to host. 23 | az vm open-port --port 8080 --resource-group $resourceGroup --name $virtualMachine --priority 103 24 | 25 | # Use CustomScript extension to install NGINX. 26 | az vm extension set --publisher Microsoft.Azure.Extensions --version 2.0 --name CustomScript --vm-name $virtualMachine --resource-group $resourceGroup --settings '{"fileUris": ["https://raw.githubusercontent.com/Azure-Samples/azure-voting-app-redis/master/jenkins-tutorial/config-jenkins.sh"],"commandToExecute": "./config-jenkins.sh"}' 27 | 28 | # Get public IP 29 | ip=$(az vm list-ip-addresses --resource-group $resourceGroup --name $virtualMachine --query [0].virtualMachine.network.publicIpAddresses[0].ipAddress -o tsv) 30 | 31 | # Copy Kube config file to Jenkins 32 | ssh -o "StrictHostKeyChecking no" $adminUser@$ip sudo chmod 777 /var/lib/jenkins 33 | yes | scp $pathToKubeConfig $adminUser@$ip:/var/lib/jenkins/config 34 | ssh -o "StrictHostKeyChecking no" $adminUser@$ip sudo chmod 777 /var/lib/jenkins/config 35 | 36 | # Get Jenkins Unlock Key 37 | url="http://$ip:8080" 38 | echo "Open a browser to $url" 39 | echo "Enter the following to Unlock Jenkins:" 40 | ssh -o "StrictHostKeyChecking no" $adminUser@$ip sudo "cat /var/lib/jenkins/secrets/initialAdminPassword" 41 | 42 | else 43 | echo "Kubernetes configuration / authentication file not found. Run az aks get-credentials to download this file." 44 | fi --------------------------------------------------------------------------------