visit
It's no secret that without clear and organized processes in place, developers may struggle to collaborate effectively, leading to delays in delivering software updates. A few years ago, the Social Discovery Group team faced the challenge of a suboptimal CI/CD process. At that time, the team used TeamCity and Octopus, each with its strengths. For instance, Octopus is convenient for deployments, while TeamCity is good for automated tests and sufficiently convenient for project builds. To construct a comprehensive and visually appealing CI/CD pipeline that is maximally convenient and flexible in configuration, it is necessary to use a mix of tools. The code was stored in a local repository on Bitbucket for several projects. The SDG team studied the issue and decided to optimize the process using the existing tools.
Key optimization goals:
What was implemented in TeamCity:
The deployment for NuGet looked as follows:
The deployment for services can be seen below:
An example of the Docker Build step is presented in the screenshot:
The differences between steps 4 and 5, as mentioned earlier, were as follows:
The %deploymentTarget%
variable served as the Environment(s) parameter, to which the corresponding stage(s) in Octopus were passed during deployment (e.g., Test, Dev). When changes were pushed to the respective branches (configured) of development teams, builds and software deployments to the corresponding test environments were automatically performed. The settings are visible in the screenshot below. To connect with Octopus, two global parameters needed to be added: octopus.apiKey and octopus.url
The screenshot below shows how this looked for the SDG team:
On the right, the previously described Lifecycle was selected. The Deploy a Package stage included fairly simple default settings.
For the Deploy Raw Kubernetes Yaml stage, the SDG team used universal self-written Yaml templates. In this example - Kubernetes Script, is explained in more detail below. Corresponding parameters marked in red were also substituted. It's worth noting that necessary global variable groups were connected in the Variables->Variable Sets menu, and project-specific variables were set in the Variables->Project menu, which had higher priority.
The code in this case looked as follows:
apiVersion: apps/v1
kind: Deployment
metadata:
name: '#{Octopus.Project.Name | ToLower}'
namespace: #{Octopus.Environment.Name | ToLower}
labels:
Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}'
spec:
replicas: #{Replicas}
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
revisionHistoryLimit: 10
progressDeadlineSeconds: 600
selector:
matchLabels:
Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}'
template:
metadata:
labels:
Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}'
spec:
volumes:
#{if usesidecar}
- name: dump-storage
persistentVolumeClaim:
claimName: dumps-#{Octopus.Environment.Name | ToLower}
#{/if}
#{if MountFolders}
#{each folder in MountFolders}
- name: volume-#{folder | ToBase64 | Replace "\W" X | ToLower}
hostPath:
path: #{folder}
type: DirectoryOrCreate
#{/each}
#{/if}
- name: logs-volume
hostPath:
path: #{LogsDir}
type: DirectoryOrCreate
- name: appsettings
secret:
secretName: #{Octopus.Project.Name | ToLower}
#{if Secrets}
#{each secret in Secrets}
- name: #{secret.name}
secret:
secretName: #{secret.name}
#{/each}
#{/if}
#{if usesidecar}
- name: diagnostics
emptyDir: {}
- name: dumps
configMap:
name: dumps
defaultMode: 511
#{/if}
containers:
- name: #{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}-container
image: #{DockerRegistry}/projectname.#{Octopus.Project.Name | ToLower}:#{Octopus.Release.Notes}
#{if resources}
resources:
#{each resource in resources}
#{resource.Key}:
#{each entry in resource.Value}
#{entry.Key}: #{entry.Value}
#{/each}
#{/each}
#{/if}
ports:
- name: http
containerPort: 80
protocol: TCP
env:
- value: "Development"
name: "ASPNETCORE_ENVIRONMENT"
- name: DD_ENV
value: "#{Octopus.Environment.Name | ToLower}"
- name: DD_SERVICE
value: "#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}"
- name: DD_VERSION
value: "1.0.0"
- name: DD_AGENT_HOST
value: "#{DatadogAgentHost}"
- name: DD_TRACE_ROUTE_TEMPLATE_RESOURCE_NAMES_ENABLED
value: "true"
- name: DD_RUNTIME_METRICS_ENABLED
value: "true"
volumeMounts:
#{if usesidecar}
- name: dump-storage
mountPath: /tmp/dumps
#{/if}
#{if MountFolders}
#{each folder in MountFolders}
- mountPath: #{folder}
name: volume-#{folder | ToBase64 | Replace "\W" X | ToLower}
#{/each}
#{/if}
- mountPath: #{LogsDir}
name: logs-volume
#{if usesidecar}
- name: diagnostics
mountPath: /tmp
#{/if}
- name: appsettings
readOnly: true
mountPath: /app/appsettings.json
subPath: appsettings.json
#{if Secrets}
#{each secret in Secrets}
- name: #{secret.name}
readOnly: true
mountPath: #{secret.mountPath}
subPath: #{secret.subPath}
#{/each}
#{/if}
readinessProbe:
httpGet:
path: hc
port: http
scheme: HTTP
initialDelaySeconds: #{InitialDelaySeconds}
imagePullPolicy: IfNotPresent
securityContext: {}
#{if usesidecar}
- name: sidecar
image: '#{DockerRegistry}/monitor:3'
command:
- /bin/sh
args:
- '-c'
- while true; do . /app/init.sh; sleep 1m;done
env:
- name: USE_MEMORY
value: '2048'
- name: PROJECT
value: "#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}"
resources: {}
volumeMounts:
- name: diagnostics
mountPath: /tmp
- name: dump-storage
mountPath: /tmp/dumps
- name: dumps
mountPath: /app/init.sh
subPath: init.sh
shareProcessNamespace: true
#{/if}
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: environment
operator: In
values:
- "#{Node}"
---
apiVersion: v1
kind: Service
metadata:
name: #{Octopus.Project.Name | ToLower}
namespace: #{Octopus.Environment.Name | ToLower}
labels:
Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}'
spec:
type: ClusterIP
selector:
Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}'
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
ingress.kubernetes.io/ssl-redirect: 'false'
nginx.ingress.kubernetes.io/ssl-redirect: 'false'
cert-manager.io/cluster-issuer: "letsencrypt-cluster-issuer"
cert-manager.io/renew-before: '#{LetsencryptRenewBefore}'
kubernetes.io/ingress.class: nginx
#{if IngressAnnotations}
#{each annotation in IngressAnnotations}
#{annotation.Key}: #{annotation.Value}
#{/each}
#{/if}
name: #{Octopus.Project.Name | ToLower}
namespace: #{Octopus.Environment.Name | ToLower}
labels:
Octopus.Kubernetes.DeploymentName: '#{Octopus.Project.Name | ToLower}-#{Octopus.Environment.Name | ToLower}'
spec:
tls:
#{if ExternalHost}
#{each host in ExternalHost}
- hosts:
- #{host}
secretName: #{Octopus.Project.Name | ToLower}-#{host | ToBase64 | Replace "\W" X | ToLower}-tls
#{/each}
#{/if}
rules:
#{if ExternalHost}
#{each host in ExternalHost}
- host: '#{host}'
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: #{Octopus.Project.Name | ToLower}
port:
name: http
#{/each}
#{/if}
#{if usesidecar}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: dumps
namespace: #{Octopus.Environment.Name | ToLower}
data:
init.sh: |-
#!/usr/bin/env bash
mem=$(ps aux | awk '{print $6}' | sort -rn | head -1)
mb=$(($mem/1024))
archiveDumpPath="/tmp/dumps/$PROJECT-$(date +"%Y%m%d%H%M%S").zip"
fullPathGc="/tmp/$PROJECT-$(date +"%Y%m%d%H%M%S").dump"
echo "mem:" $mb" project:" $PROJECT "use:" $USE_MEMORY
if [ "$mb" -gt "$USE_MEMORY" ]; then
export USE_MEMORY=$(($USE_MEMORY*2))
pid=$(dotnet-dump ps | awk '{print $1}')
dotnet-dump collect -p $pid -o $fullPathGc
zip $fullPathGc.zip $fullPathGc
mv $fullPathGc.zip $archiveDumpPath
rm $fullPathGc
fi
#{/if}
All variables in Octopus were specified in the following format in the code: '#{Octopus.Project.Name | ToLower}'
, where the last part indicates converting to lowercase.
Finally, the service dashboard looked as follows: