From fc3fa0e7d15bb3d4832965396018d1a684d7b3a4 Mon Sep 17 00:00:00 2001 From: Yingting Huang Date: Sat, 6 Jun 2020 13:49:20 +0800 Subject: [PATCH] Kubernetes devops demo --- Dockerfile | 3 + azure-pipelines.yml | 142 ++++++++++++++++++++++++++++++++ azure-vote/config_file.cfg | 5 ++ azure-vote/main.py | 88 ++++++++++++++++++++ azure-vote/static/default.css | 96 +++++++++++++++++++++ azure-vote/templates/index.html | 29 +++++++ manifests/deployment.yml | 57 +++++++++++++ manifests/service.yml | 20 +++++ 8 files changed, 440 insertions(+) create mode 100644 Dockerfile create mode 100644 azure-pipelines.yml create mode 100644 azure-vote/config_file.cfg create mode 100644 azure-vote/main.py create mode 100644 azure-vote/static/default.css create mode 100644 azure-vote/templates/index.html create mode 100644 manifests/deployment.yml create mode 100644 manifests/service.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ac10382 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM tiangolo/uwsgi-nginx-flask:python3.6 +RUN pip install redis +ADD /azure-vote /app diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..429ac0c --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,142 @@ +# Deploy to Azure Kubernetes Service +# Build and push image to Azure Container Registry; Deploy to Azure Kubernetes Service +# https://docs.microsoft.com/azure/devops/pipelines/languages/docker + +trigger: +- master + +resources: +- repo: self + +variables: + + # Container registry service connection established during pipeline creation + dockerRegistryServiceConnection: '540ecf2e-798b-4d1b-b6ec-4b7b846bc760' + imageRepository: 'azure-vote-front-devops' + containerRegistry: 'acrsea.azurecr.io' + dockerfilePath: '**/Dockerfile' + tag: '$(Build.BuildId)' + imagePullSecret: 'acrsea98100991-auth' + + # Agent VM image name + vmImageName: 'ubuntu-latest' + + # Name of the new namespace being created to deploy the PR changes. + k8sNamespaceForPR: 'review-app-$(System.PullRequest.PullRequestId)' + +stages: +- stage: Build + displayName: Build stage + jobs: + - job: Build + displayName: Build + pool: + vmImage: $(vmImageName) + steps: + - task: Docker@2 + displayName: Build and push an image to container registry + inputs: + command: buildAndPush + repository: $(imageRepository) + dockerfile: $(dockerfilePath) + containerRegistry: $(dockerRegistryServiceConnection) + tags: | + $(tag) + + - upload: manifests + artifact: manifests + +- stage: Deploy + displayName: Deploy stage + dependsOn: Build + + jobs: + - deployment: Deploy + condition: and(succeeded(), not(startsWith(variables['Build.SourceBranch'], 'refs/pull/'))) + displayName: Deploy + pool: + vmImage: $(vmImageName) + environment: 'devopsonazurevotedevops.devops' + strategy: + runOnce: + deploy: + steps: + - task: KubernetesManifest@0 + displayName: Create imagePullSecret + inputs: + action: createSecret + secretName: $(imagePullSecret) + dockerRegistryEndpoint: $(dockerRegistryServiceConnection) + + - task: KubernetesManifest@0 + displayName: Deploy to Kubernetes cluster + inputs: + action: deploy + manifests: | + $(Pipeline.Workspace)/manifests/deployment.yml + $(Pipeline.Workspace)/manifests/service.yml + imagePullSecrets: | + $(imagePullSecret) + containers: | + $(containerRegistry)/$(imageRepository):$(tag) + + - deployment: DeployPullRequest + displayName: Deploy Pull request + condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/pull/')) + pool: + vmImage: $(vmImageName) + + environment: 'devopsonazurevotedevops.$(k8sNamespaceForPR)' + strategy: + runOnce: + deploy: + steps: + - reviewApp: devops + + - task: Kubernetes@1 + displayName: 'Create a new namespace for the pull request' + inputs: + command: apply + useConfigurationFile: true + inline: '{ "kind": "Namespace", "apiVersion": "v1", "metadata": { "name": "$(k8sNamespaceForPR)" }}' + + - task: KubernetesManifest@0 + displayName: Create imagePullSecret + inputs: + action: createSecret + secretName: $(imagePullSecret) + namespace: $(k8sNamespaceForPR) + dockerRegistryEndpoint: $(dockerRegistryServiceConnection) + + - task: KubernetesManifest@0 + displayName: Deploy to the new namespace in the Kubernetes cluster + inputs: + action: deploy + namespace: $(k8sNamespaceForPR) + manifests: | + $(Pipeline.Workspace)/manifests/deployment.yml + $(Pipeline.Workspace)/manifests/service.yml + imagePullSecrets: | + $(imagePullSecret) + containers: | + $(containerRegistry)/$(imageRepository):$(tag) + + - task: Kubernetes@1 + name: get + displayName: 'Get services in the new namespace' + continueOnError: true + inputs: + command: get + namespace: $(k8sNamespaceForPR) + arguments: svc + outputFormat: jsonpath='http://{.items[0].status.loadBalancer.ingress[0].ip}:{.items[0].spec.ports[0].port}' + + # Getting the IP of the deployed service and writing it to a variable for posing comment + - script: | + url="$(get.KubectlOutput)" + message="Your review app has been deployed" + if [ ! -z "$url" -a "$url" != "http://:" ] + then + message="${message} and is available at $url.

[Learn More](https://aka.ms/testwithreviewapps) about how to test and provide feedback for the app." + fi + echo "##vso[task.setvariable variable=GITHUB_COMMENT]$message" diff --git a/azure-vote/config_file.cfg b/azure-vote/config_file.cfg new file mode 100644 index 0000000..07e9667 --- /dev/null +++ b/azure-vote/config_file.cfg @@ -0,0 +1,5 @@ +# UI Configurations +TITLE = 'Azure Voting App - Azure Devops Pipeline' +VOTE1VALUE = 'Cats' +VOTE2VALUE = 'Dogs' +SHOWHOST = 'false' \ No newline at end of file diff --git a/azure-vote/main.py b/azure-vote/main.py new file mode 100644 index 0000000..e6198ce --- /dev/null +++ b/azure-vote/main.py @@ -0,0 +1,88 @@ +from flask import Flask, request, render_template +import os +import random +import redis +import socket +import sys + +app = Flask(__name__) + +# Load configurations from environment or config file +app.config.from_pyfile('config_file.cfg') + +if ("VOTE1VALUE" in os.environ and os.environ['VOTE1VALUE']): + button1 = os.environ['VOTE1VALUE'] +else: + button1 = app.config['VOTE1VALUE'] + +if ("VOTE2VALUE" in os.environ and os.environ['VOTE2VALUE']): + button2 = os.environ['VOTE2VALUE'] +else: + button2 = app.config['VOTE2VALUE'] + +if ("TITLE" in os.environ and os.environ['TITLE']): + title = os.environ['TITLE'] +else: + title = app.config['TITLE'] + +# Redis configurations +redis_server = os.environ['REDIS'] + +# Redis Connection +try: + if "REDIS_PWD" in os.environ: + r = redis.StrictRedis(host=redis_server, + port=6379, + password=os.environ['REDIS_PWD']) + else: + r = redis.Redis(redis_server) + r.ping() +except redis.ConnectionError: + exit('Failed to connect to Redis, terminating.') + +# Change title to host name to demo NLB +if app.config['SHOWHOST'] == "true": + title = socket.gethostname() + +# Init Redis +if not r.get(button1): r.set(button1,0) +if not r.get(button2): r.set(button2,0) + +@app.route('/', methods=['GET', 'POST']) +def index(): + + if request.method == 'GET': + + # Get current values + vote1 = r.get(button1).decode('utf-8') + vote2 = r.get(button2).decode('utf-8') + + # Return index with values + return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) + + elif request.method == 'POST': + + if request.form['vote'] == 'reset': + + # Empty table and return results + r.set(button1,0) + r.set(button2,0) + vote1 = r.get(button1).decode('utf-8') + vote2 = r.get(button2).decode('utf-8') + return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) + + else: + + # Insert vote result into DB + vote = request.form['vote'] + r.incr(vote,1) + + # Get current values + vote1 = r.get(button1).decode('utf-8') + vote2 = r.get(button2).decode('utf-8') + + # Return results + return render_template("index.html", value1=int(vote1), value2=int(vote2), button1=button1, button2=button2, title=title) + +if __name__ == "__main__": + app.run() diff --git a/azure-vote/static/default.css b/azure-vote/static/default.css new file mode 100644 index 0000000..02f1cc5 --- /dev/null +++ b/azure-vote/static/default.css @@ -0,0 +1,96 @@ +body { + background-color:#F8F8F8; +} + +div#container { + margin-top:5%; +} + +div#space { + display:block; + margin: 0 auto; + width: 500px; + height: 10px; + +} + +div#logo { + display:block; + margin: 0 auto; + width: 500px; + text-align: center; + font-size:30px; + font-family:Helvetica; + /*border-bottom: 1px solid black;*/ +} + +div#form { + padding: 20px; + padding-right: 20px; + padding-top: 20px; + display:block; + margin: 0 auto; + width: 500px; + text-align: center; + font-size:30px; + font-family:Helvetica; + border-bottom: 1px solid black; + border-top: 1px solid black; +} + +div#results { + display:block; + margin: 0 auto; + width: 500px; + text-align: center; + font-size:30px; + font-family:Helvetica; +} + +.button { + background-color: #4CAF50; /* Green */ + border: none; + color: white; + padding: 16px 32px; + text-align: center; + text-decoration: none; + display: inline-block; + font-size: 16px; + margin: 4px 2px; + -webkit-transition-duration: 0.4s; /* Safari */ + transition-duration: 0.4s; + cursor: pointer; + width: 250px; +} + +.button1 { + background-color: white; + color: black; + border: 2px solid #008CBA; +} + +.button1:hover { + background-color: #008CBA; + color: white; +} +.button2 { + background-color: white; + color: black; + border: 2px solid #555555; +} + +.button2:hover { + background-color: #555555; + color: white; +} + +.button3 { + background-color: white; + color: black; + border: 2px solid #f44336; +} + +.button3:hover { + background-color: #f44336; + color: white; +} \ No newline at end of file diff --git a/azure-vote/templates/index.html b/azure-vote/templates/index.html new file mode 100644 index 0000000..603e110 --- /dev/null +++ b/azure-vote/templates/index.html @@ -0,0 +1,29 @@ + + + + + {{title}} + + + + + +
+
+ +
+
+ + + +
+
+
{{button1}} - {{ value1 }} | {{button2}} - {{ value2 }}
+ +
+
+ + \ No newline at end of file diff --git a/manifests/deployment.yml b/manifests/deployment.yml new file mode 100644 index 0000000..316858e --- /dev/null +++ b/manifests/deployment.yml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-back +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-back + template: + metadata: + labels: + app: azure-vote-back + spec: + nodeSelector: + "beta.kubernetes.io/os": linux + containers: + - name: azure-vote-back + image: redis + ports: + - containerPort: 6379 + name: redis +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-front +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-front + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + minReadySeconds: 5 + template: + metadata: + labels: + app: azure-vote-front + spec: + nodeSelector: + "beta.kubernetes.io/os": linux + containers: + - name: azure-vote-front + image: acrsea.azurecr.io/azure-vote-front-devops + ports: + - containerPort: 80 + resources: + requests: + cpu: 250m + limits: + cpu: 500m + env: + - name: REDIS + value: "azure-vote-back" \ No newline at end of file diff --git a/manifests/service.yml b/manifests/service.yml new file mode 100644 index 0000000..9f16fbb --- /dev/null +++ b/manifests/service.yml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-back +spec: + ports: + - port: 6379 + selector: + app: azure-vote-back +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-front +spec: + type: LoadBalancer + ports: + - port: 80 + selector: + app: azure-vote-front \ No newline at end of file