Build & Deployment
Jenkins, Ansible, and the Art of Not Breaking Production
π― What You'll Learn
- How Dione's build and deployment pipeline actually works
- Maven configuration and dependency management
- Jenkins CI/CD setup and pipeline stages
- Ansible deployment scripts and automation
- Environment management and rollback procedures
Build System Overview
β οΈ Deployment Reality
Dione's deployment process is semi-automated. Jenkins handles the build and basic deployment, but manual intervention is often required for production releases. It's not fully automated, but it's reliable.
Deployment Philosophy
Safety First
Multiple checkpoints and manual approvals for production deployments
Checklist Driven
Detailed deployment checklists prevent common mistakes
Environment Progression
Dev β SIT β UAT β Production with testing at each stage
Rollback Ready
Every deployment includes a tested rollback plan
Deployment Pipeline Architecture
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Developer β β Git Repositoryβ β Jenkins β
β Commits Code βββββΆβ (GitLab) βββββΆβ Build Server β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Maven Build βββββΆβ Unit Tests βββββΆβ Package WAR β
β Compile β β Integration β β Create β
β Dependencies β β Tests β β Artifacts β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
β Ansible βββββΆβ Environment βββββΆβ Health Check β
β Deployment β β Configuration β β Smoke Tests β
β Scripts β β Setup β β Verification β
βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββββ
Maven Configuration
Dione uses Maven for dependency management and build automation. The configuration is complex due to the modular structure and multiple deployment targets.
Parent POM Structure
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.currenciesdirect.gtg.ngop</groupId>
<artifactId>dione-parent</artifactId>
<version>3.2.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>services/business</module>
<module>services/dioneservices</module>
<module>customer_portal</module>
<module>admin_portal</module>
</modules>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Dependency Versions -->
<spring.version>4.3.18.RELEASE</spring.version>
<hibernate.version>5.2.17.Final</hibernate.version>
<cxf.version>3.2.7</cxf.version>
<junit.version>4.12</junit.version>
<!-- Build Properties -->
<build.timestamp>${maven.build.timestamp}</build.timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<archive>
<manifestEntries>
<Built-By>Jenkins</Built-By>
<Build-Time>${build.timestamp}</Build-Time>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<profiles>
<profile>
<id>development</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<environment>dev</environment>
</properties>
</profile>
<profile>
<id>sit</id>
<properties>
<environment>sit</environment>
</properties>
</profile>
<profile>
<id>uat</id>
<properties>
<environment>uat</environment>
</properties>
</profile>
<profile>
<id>production</id>
<properties>
<environment>prod</environment>
</properties>
</profile>
</profiles>
</project>
Module-Specific Configuration
<project>
<parent>
<groupId>com.currenciesdirect.gtg.ngop</groupId>
<artifactId>dione-parent</artifactId>
<version>3.2.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>business-services</artifactId>
<packaging>war</packaging>
<name>Dione Business Services</name>
<dependencies>
<!-- Spring Dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
</dependency>
<!-- JPA/Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<!-- Apache CXF for SOAP Services -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http</artifactId>
<version>${cxf.version}</version>
</dependency>
<!-- SQL Server Driver -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>7.4.1.jre8</version>
</dependency>
</dependencies>
<build>
<finalName>business-services-${environment}</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<webResources>
<resource>
<directory>src/main/resources/config/${environment}</directory>
<targetPath>WEB-INF/classes</targetPath>
<filtering>true</filtering>
</resource>
</webResources>
</configuration>
</plugin>
<!-- Generate WSDL documentation -->
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-java2ws-plugin</artifactId>
<version>${cxf.version}</version>
<executions>
<execution>
<id>generate-wsdl</id>
<phase>process-classes</phase>
<goals>
<goal>java2ws</goal>
</goals>
<configuration>
<className>com.currenciesdirect.gtg.ngop.business.services.auth.WSAuth</className>
<outputFile>${project.build.directory}/wsdl/AuthService.wsdl</outputFile>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Build Commands
# Clean and compile all modules
mvn clean compile
# Run tests and package for SIT environment
mvn clean package -Psit
# Skip tests and package for production
mvn clean package -Pproduction -DskipTests
# Deploy to local repository
mvn clean install
# Generate dependency tree for debugging
mvn dependency:tree
# Check for newer versions of dependencies
mvn versions:display-dependency-updates
# Create deployment artifacts
mvn clean package -Pproduction -DskipTests assembly:single
Jenkins CI/CD Pipeline
Jenkins orchestrates our build and deployment process with separate pipelines for different environments.
Pipeline Configuration
pipeline {
agent {
label 'maven-build-slave'
}
environment {
MAVEN_HOME = '/opt/maven'
JAVA_HOME = '/opt/java/openjdk-8'
PATH = "${MAVEN_HOME}/bin:${JAVA_HOME}/bin:${PATH}"
// Deployment settings
DEPLOY_ENV = 'production'
WAR_FILE_NAME = "business-services-${DEPLOY_ENV}.war"
BACKUP_DIR = "/opt/deployments/backups"
DEPLOY_DIR = "/opt/jboss/standalone/deployments"
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 30, unit: 'MINUTES')
timestamps()
}
stages {
stage('Checkout') {
steps {
script {
// Checkout specific branch or tag
checkout scm
// Get commit info for tracking
env.GIT_COMMIT_SHORT = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.BUILD_VERSION = "${env.BUILD_NUMBER}-${env.GIT_COMMIT_SHORT}"
}
echo "Building version: ${env.BUILD_VERSION}"
}
}
stage('Build') {
steps {
script {
try {
// Clean and compile
sh 'mvn clean compile -P${DEPLOY_ENV}'
// Check for common issues
sh '''
# Check for missing properties files
find . -name "*.properties.template" | while read template; do
actual="${template%.template}"
if [ ! -f "$actual" ]; then
echo "ERROR: Missing properties file: $actual"
exit 1
fi
done
'''
} catch (Exception e) {
error "Build failed: ${e.getMessage()}"
}
}
}
}
stage('Test') {
steps {
script {
try {
// Run unit tests
sh 'mvn test -P${DEPLOY_ENV}'
// Run integration tests (if not production)
if (env.DEPLOY_ENV != 'production') {
sh 'mvn failsafe:integration-test -P${DEPLOY_ENV}'
}
} catch (Exception e) {
// Don't fail production builds on test failure, but warn
if (env.DEPLOY_ENV == 'production') {
echo "WARNING: Tests failed, but continuing with production build"
currentBuild.result = 'UNSTABLE'
} else {
error "Tests failed: ${e.getMessage()}"
}
}
}
}
post {
always {
// Archive test results
publishTestResults testResultsPattern: '**/target/surefire-reports/*.xml'
publishTestResults testResultsPattern: '**/target/failsafe-reports/*.xml'
}
}
}
stage('Package') {
steps {
script {
try {
// Package WAR files
sh 'mvn package -P${DEPLOY_ENV} -DskipTests'
// Verify WAR files were created
sh '''
for war in $(find . -name "*.war" -type f); do
echo "Created WAR: $war"
echo "Size: $(du -h $war | cut -f1)"
# Basic WAR validation
if ! unzip -t "$war" > /dev/null 2>&1; then
echo "ERROR: Invalid WAR file: $war"
exit 1
fi
done
'''
// Archive artifacts
archiveArtifacts artifacts: '**/target/*.war', fingerprint: true
} catch (Exception e) {
error "Packaging failed: ${e.getMessage()}"
}
}
}
}
stage('Security Scan') {
when {
environment name: 'DEPLOY_ENV', value: 'production'
}
steps {
script {
try {
// OWASP dependency check
sh 'mvn org.owasp:dependency-check-maven:check'
// Archive security report
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'target',
reportFiles: 'dependency-check-report.html',
reportName: 'Security Scan Report'
])
} catch (Exception e) {
echo "Security scan failed: ${e.getMessage()}"
currentBuild.result = 'UNSTABLE'
}
}
}
}
stage('Deploy') {
when {
anyOf {
branch 'master'
branch 'release/*'
}
}
steps {
script {
// Require manual approval for production
if (env.DEPLOY_ENV == 'production') {
input message: 'Deploy to Production?',
ok: 'Deploy',
submitterParameter: 'DEPLOYER'
echo "Production deployment approved by: ${env.DEPLOYER}"
}
// Call Ansible deployment
build job: 'ansible-deploy-dione',
parameters: [
string(name: 'ENVIRONMENT', value: env.DEPLOY_ENV),
string(name: 'BUILD_NUMBER', value: env.BUILD_NUMBER),
string(name: 'WAR_ARTIFACT_URL', value: "${env.BUILD_URL}artifact/services/business/target/${WAR_FILE_NAME}")
]
}
}
}
}
post {
always {
// Clean workspace
cleanWs()
}
success {
// Notify teams on successful production deployment
script {
if (env.DEPLOY_ENV == 'production') {
slackSend channel: '#deployment-notifications',
color: 'good',
message: "β
Dione ${env.BUILD_VERSION} successfully deployed to production"
}
}
}
failure {
// Notify teams on failure
slackSend channel: '#deployment-notifications',
color: 'danger',
message: "β Dione build ${env.BUILD_NUMBER} failed in ${env.STAGE_NAME} stage"
}
unstable {
// Notify teams on unstable build
slackSend channel: '#deployment-notifications',
color: 'warning',
message: "β οΈ Dione build ${env.BUILD_NUMBER} completed with warnings"
}
}
}
Jenkins Job Configuration
JOB CONFIGURATION:
Build Triggers:
- Poll SCM: H/5 * * * * (every 5 minutes)
- GitHub webhook for push events
- Manual trigger with parameters
Build Environment:
- Delete workspace before build starts
- Set build timeout: 30 minutes
- Use custom workspace: /var/jenkins_home/workspace/dione-${ENVIRONMENT}
Build Parameters:
- Environment: Choice (dev, sit, uat, production)
- Skip Tests: Boolean (default: false)
- Deploy After Build: Boolean (default: true)
- Git Branch: String (default: master)
Post-build Actions:
- Archive artifacts: **/*.war
- Publish JUnit results: **/target/surefire-reports/*.xml
- Slack notifications
- Email notifications to team leads
Ansible Deployment
Ansible handles the actual deployment to servers, configuration management, and service orchestration.
Deployment Playbook
---
- name: Deploy Dione Application
hosts: "{{ target_environment }}"
become: yes
serial: 1 # Deploy one server at a time
vars:
app_name: dione
app_user: jboss
jboss_home: /opt/jboss
deploy_dir: "{{ jboss_home }}/standalone/deployments"
backup_dir: "/opt/deployments/backups"
war_file: "business-services-{{ environment }}.war"
health_check_url: "http://localhost:8080/business-services/health"
pre_tasks:
- name: Check if this is a production deployment
set_fact:
is_production: "{{ environment == 'production' }}"
- name: Require confirmation for production deployment
pause:
prompt: "This will deploy to PRODUCTION. Type 'yes' to continue"
when: is_production
register: production_confirm
- name: Fail if production deployment not confirmed
fail:
msg: "Production deployment cancelled"
when: is_production and production_confirm.user_input != "yes"
tasks:
- name: Create backup directory
file:
path: "{{ backup_dir }}/{{ ansible_date_time.epoch }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
- name: Stop JBoss service
systemd:
name: jboss
state: stopped
register: jboss_stop
- name: Wait for JBoss to fully stop
wait_for:
port: 8080
host: localhost
state: stopped
timeout: 60
- name: Backup current deployment
copy:
src: "{{ deploy_dir }}/{{ war_file }}"
dest: "{{ backup_dir }}/{{ ansible_date_time.epoch }}/{{ war_file }}"
remote_src: yes
owner: "{{ app_user }}"
group: "{{ app_user }}"
ignore_errors: yes # File might not exist on first deployment
- name: Remove old deployment
file:
path: "{{ deploy_dir }}/{{ war_file }}"
state: absent
- name: Remove deployment markers
file:
path: "{{ deploy_dir }}/{{ war_file }}.deployed"
state: absent
- name: Download new WAR file from Jenkins
get_url:
url: "{{ war_artifact_url }}"
dest: "{{ deploy_dir }}/{{ war_file }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0644'
timeout: 300
register: war_download
- name: Verify WAR file integrity
command: unzip -t "{{ deploy_dir }}/{{ war_file }}"
register: war_verify
failed_when: war_verify.rc != 0
- name: Update configuration files
template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0644'
backup: yes
loop:
- { src: "ngop.properties.j2", dest: "{{ jboss_home }}/standalone/configuration/ngop.properties" }
- { src: "standalone.xml.j2", dest: "{{ jboss_home }}/standalone/configuration/standalone.xml" }
notify:
- restart jboss
- name: Start JBoss service
systemd:
name: jboss
state: started
enabled: yes
register: jboss_start
- name: Wait for JBoss to start
wait_for:
port: 8080
host: localhost
state: started
timeout: 300
- name: Wait for application deployment
uri:
url: "{{ health_check_url }}"
method: GET
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 10
- name: Verify deployment success
uri:
url: "{{ health_check_url }}"
method: GET
return_content: yes
register: health_response
- name: Display deployment status
debug:
msg: |
Deployment Status: SUCCESS
Application Version: {{ health_response.json.version | default('Unknown') }}
Deployment Time: {{ ansible_date_time.iso8601 }}
Server: {{ inventory_hostname }}
handlers:
- name: restart jboss
systemd:
name: jboss
state: restarted
post_tasks:
- name: Run smoke tests
include_tasks: smoke-tests.yml
- name: Clean old backups (keep last 5)
shell: |
cd {{ backup_dir }}
ls -1t | tail -n +6 | xargs -r rm -rf
- name: Send deployment notification
uri:
url: "{{ slack_webhook_url }}"
method: POST
body_format: json
body:
text: "β
Dione deployed successfully to {{ environment }} on {{ inventory_hostname }}"
when: slack_webhook_url is defined
Configuration Templates
# Dione Configuration for {{ environment }} environment
# Generated on {{ ansible_date_time.iso8601 }}
# Database Configuration
jdbc.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
jdbc.url=jdbc:sqlserver://{{ db_host }}:{{ db_port }};databaseName={{ db_name }};integratedSecurity=false
jdbc.username={{ db_username }}
jdbc.password={{ db_password }}
# Connection Pool Settings
jdbc.initialPoolSize={{ db_pool_initial_size | default(5) }}
jdbc.minPoolSize={{ db_pool_min_size | default(5) }}
jdbc.maxPoolSize={{ db_pool_max_size | default(50) }}
jdbc.maxIdleTime={{ db_pool_max_idle_time | default(300) }}
# External Service URLs
{% if environment == 'production' %}
ngop.sf.connectionurl={{ salesforce_prod_url }}
pe.serivce.host={{ pricing_engine_prod_host }}
commhub.service.url={{ commhub_prod_url }}
{% else %}
ngop.sf.connectionurl={{ salesforce_test_url }}
pe.serivce.host={{ pricing_engine_test_host }}
commhub.service.url={{ commhub_test_url }}
{% endif %}
# Email Configuration
email.smtp.host={{ smtp_host }}
email.smtp.port={{ smtp_port }}
email.smtp.username={{ smtp_username }}
email.smtp.password={{ smtp_password }}
email.smtp.auth={{ smtp_auth | default('true') }}
email.smtp.starttls.enable={{ smtp_starttls | default('true') }}
# Logging Configuration
log.level.root={{ log_level_root | default('INFO') }}
log.level.application={{ log_level_app | default('DEBUG') }}
log.file.path={{ log_file_path | default('/opt/jboss/logs/application.log') }}
# Cache Configuration
cache.provider=infinispan
cache.config.file={{ jboss_home }}/standalone/configuration/infinispan-config.xml
# Environment Specific Settings
environment.name={{ environment }}
deployment.timestamp={{ ansible_date_time.epoch }}
deployment.version={{ app_version | default('unknown') }}
Inventory Configuration
all:
children:
production:
hosts:
dione-prod-web01:
ansible_host: 10.0.1.10
server_role: web
dione-prod-web02:
ansible_host: 10.0.1.11
server_role: web
dione-prod-app01:
ansible_host: 10.0.1.20
server_role: app
dione-prod-app02:
ansible_host: 10.0.1.21
server_role: app
vars:
environment: production
# Database Configuration
db_host: prod-sql-cluster.internal
db_port: 1433
db_name: NGOP_PROD_DB
db_username: "{{ vault_db_username }}"
db_password: "{{ vault_db_password }}"
# Connection Pool Sizing
db_pool_initial_size: 10
db_pool_min_size: 10
db_pool_max_size: 100
db_pool_max_idle_time: 300
# External Services
salesforce_prod_url: "https://login.salesforce.com/services/Soap/c/38.0"
pricing_engine_prod_host: "pricing-prod.internal"
commhub_prod_url: "https://commhub-prod.internal/commhub/"
# Email Configuration
smtp_host: "smtp.office365.com"
smtp_port: 587
smtp_username: "{{ vault_smtp_username }}"
smtp_password: "{{ vault_smtp_password }}"
# Logging
log_level_root: "WARN"
log_level_app: "INFO"
log_file_path: "/opt/jboss/logs/dione-production.log"
# Notification
slack_webhook_url: "{{ vault_slack_webhook_url }}"
Environment Management
Managing multiple environments with different configurations and deployment schedules.
Environment Hierarchy
ENVIRONMENT PROGRESSION:
Development (DEV)
βββ Purpose: Developer testing and integration
βββ Database: Shared dev database with test data
βββ External Services: Mock services or dev endpoints
βββ Deployment: Automatic on commit to develop branch
βββ Monitoring: Basic application logs
βββ Access: All developers
System Integration Testing (SIT)
βββ Purpose: Integration testing and QA validation
βββ Database: Dedicated SIT database with production-like data
βββ External Services: Test endpoints, limited functionality
βββ Deployment: Manual trigger after dev validation
βββ Monitoring: Application + performance monitoring
βββ Access: QA team, senior developers
User Acceptance Testing (UAT)
βββ Purpose: Business user testing and sign-off
βββ Database: Production copy (sanitized)
βββ External Services: Production-like test endpoints
βββ Deployment: Manual trigger after SIT approval
βββ Monitoring: Full monitoring stack
βββ Access: Business users, product owners
Production (PROD)
βββ Purpose: Live customer-facing environment
βββ Database: Production database cluster
βββ External Services: Live production endpoints
βββ Deployment: Manual trigger with approvals
βββ Monitoring: Full monitoring + alerting
βββ Access: Limited to production support team
Configuration Management
# Environment configuration script
#!/bin/bash
ENVIRONMENT=$1
CONFIG_DIR="/opt/dione/config"
case $ENVIRONMENT in
"dev")
echo "Configuring Development environment..."
# Database settings
DB_HOST="dev-sql.internal"
DB_NAME="NGOP_DEV_DB"
DB_POOL_SIZE="5"
# External services
USE_MOCK_SERVICES="true"
PRICING_ENGINE_HOST="mock-pricing.dev"
SALESFORCE_URL="https://test.salesforce.com"
# Logging
LOG_LEVEL="DEBUG"
LOG_TO_CONSOLE="true"
;;
"sit")
echo "Configuring SIT environment..."
# Database settings
DB_HOST="sit-sql.internal"
DB_NAME="NGOP_SIT_DB"
DB_POOL_SIZE="10"
# External services
USE_MOCK_SERVICES="false"
PRICING_ENGINE_HOST="pricing-sit.internal"
SALESFORCE_URL="https://test.salesforce.com"
# Logging
LOG_LEVEL="INFO"
LOG_TO_CONSOLE="false"
;;
"uat")
echo "Configuring UAT environment..."
# Database settings
DB_HOST="uat-sql.internal"
DB_NAME="NGOP_UAT_DB"
DB_POOL_SIZE="15"
# External services
USE_MOCK_SERVICES="false"
PRICING_ENGINE_HOST="pricing-uat.internal"
SALESFORCE_URL="https://test.salesforce.com"
# Logging
LOG_LEVEL="INFO"
LOG_TO_CONSOLE="false"
;;
"production")
echo "Configuring Production environment..."
# Database settings
DB_HOST="prod-sql-cluster.internal"
DB_NAME="NGOP_PROD_DB"
DB_POOL_SIZE="50"
# External services
USE_MOCK_SERVICES="false"
PRICING_ENGINE_HOST="pricing-prod.internal"
SALESFORCE_URL="https://login.salesforce.com"
# Logging
LOG_LEVEL="WARN"
LOG_TO_CONSOLE="false"
;;
*)
echo "Unknown environment: $ENVIRONMENT"
exit 1
;;
esac
# Generate configuration file
cat > $CONFIG_DIR/ngop.properties << EOF
# Generated configuration for $ENVIRONMENT environment
# Timestamp: $(date)
jdbc.url=jdbc:sqlserver://$DB_HOST:1433;databaseName=$DB_NAME
jdbc.maxPoolSize=$DB_POOL_SIZE
use.mock.services=$USE_MOCK_SERVICES
pe.service.host=$PRICING_ENGINE_HOST
sf.connection.url=$SALESFORCE_URL
log.level.root=$LOG_LEVEL
log.console.enabled=$LOG_TO_CONSOLE
environment.name=$ENVIRONMENT
EOF
echo "Configuration generated: $CONFIG_DIR/ngop.properties"
Rollback Procedures
When deployments go wrong, having a tested rollback plan is critical for financial services.
Rollback Playbook
---
- name: Rollback Dione Application
hosts: "{{ target_environment }}"
become: yes
serial: 1
vars:
app_name: dione
app_user: jboss
jboss_home: /opt/jboss
deploy_dir: "{{ jboss_home }}/standalone/deployments"
backup_dir: "/opt/deployments/backups"
war_file: "business-services-{{ environment }}.war"
pre_tasks:
- name: Check if this is a production rollback
set_fact:
is_production: "{{ environment == 'production' }}"
- name: Require confirmation for production rollback
pause:
prompt: "This will ROLLBACK PRODUCTION. Type 'rollback' to continue"
when: is_production
register: production_confirm
- name: Fail if production rollback not confirmed
fail:
msg: "Production rollback cancelled"
when: is_production and production_confirm.user_input != "rollback"
- name: Find latest backup
find:
paths: "{{ backup_dir }}"
file_type: directory
use_regex: yes
patterns: '^[0-9]+$'
register: backup_dirs
- name: Set backup directory to use
set_fact:
latest_backup: "{{ backup_dirs.files | sort(attribute='ctime', reverse=true) | first }}"
when: backup_dirs.files | length > 0
- name: Fail if no backup found
fail:
msg: "No backup found for rollback"
when: backup_dirs.files | length == 0
tasks:
- name: Display rollback information
debug:
msg: |
Rolling back to backup from: {{ latest_backup.path }}
Backup created: {{ latest_backup.ctime }}
Current time: {{ ansible_date_time.iso8601 }}
- name: Stop JBoss service
systemd:
name: jboss
state: stopped
register: jboss_stop
- name: Wait for JBoss to fully stop
wait_for:
port: 8080
host: localhost
state: stopped
timeout: 60
- name: Create emergency backup of current state
copy:
src: "{{ deploy_dir }}/{{ war_file }}"
dest: "{{ backup_dir }}/emergency_backup_{{ ansible_date_time.epoch }}/{{ war_file }}"
remote_src: yes
ignore_errors: yes
- name: Remove current deployment
file:
path: "{{ deploy_dir }}/{{ war_file }}"
state: absent
- name: Remove deployment markers
file:
path: "{{ deploy_dir }}/{{ war_file }}.deployed"
state: absent
- name: Restore previous version
copy:
src: "{{ latest_backup.path }}/{{ war_file }}"
dest: "{{ deploy_dir }}/{{ war_file }}"
remote_src: yes
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0644'
- name: Restore previous configuration
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
remote_src: yes
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0644'
loop:
- { src: "{{ latest_backup.path }}/ngop.properties", dest: "{{ jboss_home }}/standalone/configuration/ngop.properties" }
- { src: "{{ latest_backup.path }}/standalone.xml", dest: "{{ jboss_home }}/standalone/configuration/standalone.xml" }
ignore_errors: yes # Config files might not be in backup
- name: Start JBoss service
systemd:
name: jboss
state: started
register: jboss_start
- name: Wait for JBoss to start
wait_for:
port: 8080
host: localhost
state: started
timeout: 300
- name: Wait for application to be available
uri:
url: "http://localhost:8080/business-services/health"
method: GET
status_code: 200
register: health_check
until: health_check.status == 200
retries: 30
delay: 10
- name: Verify rollback success
uri:
url: "http://localhost:8080/business-services/health"
method: GET
return_content: yes
register: health_response
- name: Display rollback status
debug:
msg: |
Rollback Status: SUCCESS
Restored Version: {{ health_response.json.version | default('Unknown') }}
Rollback Time: {{ ansible_date_time.iso8601 }}
Server: {{ inventory_hostname }}
post_tasks:
- name: Run post-rollback smoke tests
include_tasks: smoke-tests.yml
- name: Send rollback notification
uri:
url: "{{ slack_webhook_url }}"
method: POST
body_format: json
body:
text: "π Dione rolled back on {{ environment }} - {{ inventory_hostname }}"
when: slack_webhook_url is defined
Emergency Rollback Script
#!/bin/bash
# Emergency rollback script for critical production issues
# Usage: ./emergency-rollback.sh [environment]
set -e
ENVIRONMENT=${1:-production}
JBOSS_HOME="/opt/jboss"
DEPLOY_DIR="$JBOSS_HOME/standalone/deployments"
BACKUP_DIR="/opt/deployments/backups"
WAR_FILE="business-services-${ENVIRONMENT}.war"
echo "=== EMERGENCY ROLLBACK INITIATED ==="
echo "Environment: $ENVIRONMENT"
echo "Timestamp: $(date)"
echo "User: $(whoami)"
# Verify we're running as the correct user
if [ "$(whoami)" != "jboss" ]; then
echo "ERROR: Must run as jboss user"
exit 1
fi
# Find the latest backup
LATEST_BACKUP=$(find $BACKUP_DIR -maxdepth 1 -type d -name "[0-9]*" | sort -nr | head -1)
if [ -z "$LATEST_BACKUP" ]; then
echo "ERROR: No backup found"
exit 1
fi
echo "Using backup: $LATEST_BACKUP"
echo "Backup created: $(stat -c %y $LATEST_BACKUP)"
# Confirm rollback
if [ "$ENVIRONMENT" = "production" ]; then
echo ""
echo "WARNING: This will rollback PRODUCTION!"
read -p "Type 'rollback' to continue: " CONFIRM
if [ "$CONFIRM" != "rollback" ]; then
echo "Rollback cancelled"
exit 1
fi
fi
echo "Starting rollback process..."
# Stop JBoss
echo "Stopping JBoss..."
sudo systemctl stop jboss
# Wait for JBoss to stop
echo "Waiting for JBoss to stop..."
sleep 10
# Check if JBoss is really stopped
if pgrep -f jboss > /dev/null; then
echo "Force killing JBoss processes..."
sudo pkill -f jboss
sleep 5
fi
# Create emergency backup of current state
EMERGENCY_BACKUP="$BACKUP_DIR/emergency_$(date +%s)"
mkdir -p "$EMERGENCY_BACKUP"
if [ -f "$DEPLOY_DIR/$WAR_FILE" ]; then
echo "Creating emergency backup..."
cp "$DEPLOY_DIR/$WAR_FILE" "$EMERGENCY_BACKUP/"
fi
# Remove current deployment
echo "Removing current deployment..."
rm -f "$DEPLOY_DIR/$WAR_FILE"
rm -f "$DEPLOY_DIR/$WAR_FILE.deployed"
rm -f "$DEPLOY_DIR/$WAR_FILE.failed"
# Restore from backup
echo "Restoring from backup..."
if [ ! -f "$LATEST_BACKUP/$WAR_FILE" ]; then
echo "ERROR: WAR file not found in backup: $LATEST_BACKUP/$WAR_FILE"
exit 1
fi
cp "$LATEST_BACKUP/$WAR_FILE" "$DEPLOY_DIR/"
# Start JBoss
echo "Starting JBoss..."
sudo systemctl start jboss
# Wait for JBoss to start
echo "Waiting for JBoss to start..."
for i in {1..60}; do
if curl -f http://localhost:8080/business-services/health > /dev/null 2>&1; then
echo "JBoss started successfully"
break
fi
echo "Waiting... ($i/60)"
sleep 5
done
# Verify application is working
if curl -f http://localhost:8080/business-services/health > /dev/null 2>&1; then
echo "=== ROLLBACK SUCCESSFUL ==="
echo "Application is responding"
# Send notification
if [ -n "$SLACK_WEBHOOK" ]; then
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"π EMERGENCY ROLLBACK completed on '$(hostname)' - '$(date)'"}' \
"$SLACK_WEBHOOK"
fi
else
echo "=== ROLLBACK FAILED ==="
echo "Application is not responding"
exit 1
fi
Deployment Monitoring
Monitoring deployment health and application performance after releases.
Health Check Endpoints
@RestController
@RequestMapping("/health")
public class HealthCheckController {
@Autowired
private DataSource dataSource;
@Autowired
private CacheDataConsumer cacheDataConsumer;
@Value("${build.version:unknown}")
private String buildVersion;
@Value("${build.timestamp:unknown}")
private String buildTimestamp;
@RequestMapping(method = RequestMethod.GET)
public ResponseEntity
Deployment Monitoring Script
#!/bin/bash
# Post-deployment monitoring script
ENVIRONMENT=$1
HEALTH_URL="http://localhost:8080/business-services/health"
MONITORING_DURATION=300 # 5 minutes
CHECK_INTERVAL=10 # 10 seconds
echo "Starting post-deployment monitoring for $ENVIRONMENT"
echo "Duration: ${MONITORING_DURATION}s, Interval: ${CHECK_INTERVAL}s"
START_TIME=$(date +%s)
SUCCESS_COUNT=0
FAILURE_COUNT=0
RESPONSE_TIMES=()
while [ $(($(date +%s) - START_TIME)) -lt $MONITORING_DURATION ]; do
# Perform health check
START_CHECK=$(date +%s%3N)
if RESPONSE=$(curl -s -w "%{http_code}" -o /tmp/health_response.json "$HEALTH_URL" 2>/dev/null); then
HTTP_CODE="${RESPONSE: -3}"
END_CHECK=$(date +%s%3N)
RESPONSE_TIME=$((END_CHECK - START_CHECK))
if [ "$HTTP_CODE" = "200" ]; then
SUCCESS_COUNT=$((SUCCESS_COUNT + 1))
RESPONSE_TIMES+=($RESPONSE_TIME)
# Extract key metrics from response
VERSION=$(jq -r '.version // "unknown"' /tmp/health_response.json 2>/dev/null)
DB_STATUS=$(jq -r '.database.status // "unknown"' /tmp/health_response.json 2>/dev/null)
CACHE_STATUS=$(jq -r '.cache.status // "unknown"' /tmp/health_response.json 2>/dev/null)
echo "[$(date '+%H:%M:%S')] β
Health check passed (${RESPONSE_TIME}ms) - DB:$DB_STATUS Cache:$CACHE_STATUS Version:$VERSION"
else
FAILURE_COUNT=$((FAILURE_COUNT + 1))
echo "[$(date '+%H:%M:%S')] β Health check failed - HTTP $HTTP_CODE"
fi
else
FAILURE_COUNT=$((FAILURE_COUNT + 1))
echo "[$(date '+%H:%M:%S')] β Health check failed - Connection error"
fi
sleep $CHECK_INTERVAL
done
# Calculate statistics
TOTAL_CHECKS=$((SUCCESS_COUNT + FAILURE_COUNT))
SUCCESS_RATE=$(echo "scale=2; $SUCCESS_COUNT * 100 / $TOTAL_CHECKS" | bc)
if [ ${#RESPONSE_TIMES[@]} -gt 0 ]; then
# Calculate average response time
TOTAL_TIME=0
for time in "${RESPONSE_TIMES[@]}"; do
TOTAL_TIME=$((TOTAL_TIME + time))
done
AVG_RESPONSE_TIME=$(echo "scale=2; $TOTAL_TIME / ${#RESPONSE_TIMES[@]}" | bc)
else
AVG_RESPONSE_TIME="N/A"
fi
echo ""
echo "=== DEPLOYMENT MONITORING SUMMARY ==="
echo "Environment: $ENVIRONMENT"
echo "Duration: ${MONITORING_DURATION}s"
echo "Total Checks: $TOTAL_CHECKS"
echo "Successful: $SUCCESS_COUNT"
echo "Failed: $FAILURE_COUNT"
echo "Success Rate: ${SUCCESS_RATE}%"
echo "Average Response Time: ${AVG_RESPONSE_TIME}ms"
# Determine overall health
if (( $(echo "$SUCCESS_RATE >= 95" | bc -l) )); then
echo "Status: β
HEALTHY"
exit 0
elif (( $(echo "$SUCCESS_RATE >= 80" | bc -l) )); then
echo "Status: β οΈ DEGRADED"
exit 1
else
echo "Status: β UNHEALTHY"
exit 2
fi
π― Deployment Best Practices
- Safety First: Multiple approval gates for production deployments
- Automated Testing: Comprehensive test suite before deployment
- Blue-Green Strategy: Deploy to one server at a time
- Rollback Plan: Always have a tested rollback procedure
- Monitoring: Monitor application health after deployment
- Communication: Clear notifications to stakeholders