Motivación
En el ámbito de gestión del ciclo de vida de un proyecto o producto software, solemos separar el propio código de la configuración que depende del entorno, p.ej. integraciones con sistemas externos, configuración JVM, pooles, etc.
La configuración dependiente del entorno solemos gestionarla en carpetas separadas en el mismo proyecto, por ejemplo mediante perfiles de Maven.
Sin embargo la configuración dependiente del entorno tiene un ciclo de vida separado aunque no independiente. Además, sobre todo en entornos empresariales grandes, la configuración es mantenida por personas distintas al equipo de desarrollo.
Los cambios de arquitectura (integraciones, etc.) del proyecto provocan cambios de configuración en todos los entornos y a la vez es necesario respetar las configuraciones específicas de cada entorno (configuración no-funcional como pools de conexiones, políticas del garbage collector, etc.).
Adicionalmente no sólo existe una línea de cambios - una release - sino también varias paralelas como por ejemplo la evolución a una nueva arquitectura, pruebas de concepto, migración a una nueva infraestructura o Middleware, hotfixes, etc.
Por lo tanto resulta necesario facilitar un merge, no sólo del código fuente sino también de las configuraciones dependientes del entorno.
Solución ad-hoc con Gradle
A continuación os enseño una posible solución con Gradle. Para una introducción recomiendo la documentación de Gradle (actualmente v2.13):
Aplicación con dependencia de configuración
Sobre cada proyecto desplegable (war, ear, jar, zip, etc.) se configura una dependencia, p.ej. envConfig
.
dependencies {
// other dependencies like compile, test, etc.
envConfig 'com.ats.gradle.dummy:gradle-dummy-env:2.3.0-SNAPSHOT'
}
En el momento de empaquetar el proyecto dejamos esta dependencia como meta-información en el MANIFEST.MF
de la siguiente manera.
Manifest-Version: 1.0
Implementation-Title: atSistemas Gradle Dummy
Implementation-Version: 3.1.0-SNAPSHOT
EnvConfig-Dependency-Group: com.ats.gradle.dummy
EnvConfig-Dependency-Name: gradle-dummy-env
EnvConfig-Dependency-Version: 2.3.0-SNAPSHOT
Esta funcionalidad requiere cierta configuración del proyecto de aplicación que - en la vida real - debe evolucionar en un plugin común con su propio ciclo de vida.
// build.gradle - application
...
configurations.create("envConfig")
afterEvaluate {
archivesBaseName = baseName
jar {
manifest {
attributes 'Implementation-Title': 'atSistemas Gradle Dummy',
'Implementation-Version': version
}
}
def envConfigDependency
def envConfigSet = configurations.getByName('envConfig').dependencies
if (!envConfigSet.isEmpty()) {
envConfigSet.each { envConfigDependency = it }
jar.manifest {
attributes(
"EnvConfig-Dependency-Group":"${envConfigDependency.group}",
"EnvConfig-Dependency-Name":"${envConfigDependency.name}",
"EnvConfig-Dependency-Version":"${envConfigDependency.version}"
)
}
}
}
Nota: Tomamos nota de que la dependencia declarada envConfig 'com.ats.gradle.dummy:gradle-dummy-env:2.3.0-SNAPSHOT'
no existe. En el momento de configuración del entorno se añadirá un sufijo del entorno, p.ej. local, dev, int, etc.
Adelantamos que en el caso de releases declararíamos la dependencia con una versión implícita, p.ej. version: 2.3.+
Configuración del entorno
Independiente de los proyectos de aplicaciones, existen proyectos de configuración del entorno: para cada entorno un proyecto con su propio ciclo de vida.
En este sentido, el paso entre entornos es un merge, por ejemplo de la rama de release del entorno int a la rama develop del entorno de pre.
Los proyectos de configuración de entorno son del tipo zip
y contienen la configuración dependiente del entorno, por ejemplo:
- Ficheros de propiedades.
- Ficheros de configuración de runtimes:
- server.xml
- httpd.conf
- standalone.xml
- nginx.conf
- etc.
- Init scripts.
- Configuración de acceso a máquinas destino:
- Host
- deploy user
- Key file
- etc.
- Configuración de infraestuctura.
- etc.
Una posible estructura del proyecto de configuración del entorno de int podría ser la siguiente:
gradle-dummy-env-int:
├── config
│ ├── nginx
│ │ ├── www1
│ │ │ └── nginx.conf
│ │ └── www2
│ │ └── nginx.conf
│ └── tomcat
│ │ ├── node1
│ │ │ └── server.xml
│ │ └── node2
│ │ └── server.xml
│ ├── app
│ │ └── app_env.properties
│ └── env.yml
├── build.gradle
└── gradle.properties
El build.gradle queda muy simple y no tiene más misterio:
// build.gradle - environment config
apply plugin: 'maven'
task distZip(type:Zip){
from('config/') {
into('/')
}
}
artifacts {
archives(distZip.archivePath) {
type 'zip'
}
}
archivesBaseName = baseName
Uniendo las piezas: el despliegue
Vamos a utilizar la palabra despliegue en su significado más ámplio, incluyendo las siguientes acciones:
- Provisionamiento del entorno.
- Configuración del entorno.
- Distribución de la aplicación.
- Puesta en marcha.
Para unir las piezas aplicación y configuración del entorno utilizamos un proyecto Gradle genérico que recibe como parámetros:
- GAV de aplicación:
- p.ej.
com.ats.gradle.dummy:gradle-dummy-app:3.1.0-SNAPSHOT
- Nombre del entorno:
- p.ej.
int
Básicamente el proyecto realiza las siguientes acciones:
- Obtener la aplicación del repositorio.
- Extraer GAV de EnvConfig-Dependency del MANIFEST.MF de la aplicación.
- Resolver la configuración de entorno añadiendo el entorno como sufijo, p.ej. com.ats.gradle.dummy:gradle-dummy-env-int:2.3.0-SNAPSHOT
// build.gradle - deployment consumer
project.apply plugin: 'base'
repositories {
mavenCentral()
}
task('downloadEnvConfig') <<
{
// download application
def outputDir = new File("${buildDir}/deploy/app")
def artifact = project.appGAV
configurations.create("downloadApp")
dependencies {
downloadApp "${artifact}"
}
logger.lifecycle("Resolving ${artifact}...")
copy {
from {
configurations.downloadApp
}
into {
outputDir
}
}
// expand environment configuration
def envDir = new File("${buildDir}/deploy/envConfig")
def env = project.ext.environment
logger.lifecycle("Retrieving config for ${env}...")
def file = outputDir.listFiles()[0]
logger.lifecycle("Detected application: ${file}")
logger.lifecycle("Reading MANIFEST.MF attributes...")
def attributes = new java.util.jar.JarFile(file).manifest.mainAttributes
attributes.entrySet().each {
logger.info('\t${it.key}: ${it.value}')
def group = attributes.getValue("EnvConfig-Dependency-Group")
def name = attributes.getValue("EnvConfig-Dependency-Name")
def version = attributes.getValue("EnvConfig-Dependency-Version")
def envConfig = "${group}:${name}-${env}:${version}"
logger.lifecycle('Using environment configuration: ${envConfig}')
configurations.create("downloadEnvConfig")
dependencies {
downloadEnvConfig "${envConfig}"
}
logger.lifecycle('Expanding config...')
project.copy {
from {
project.configurations.downloadEnvConfig.collect {
project.zipTree(it)
}
}
into {
envDir
}
}
}
}
Si ejecutamos este script con gradle downloadEnvConfig -PappGAV=com.ats.gradle.dummy:gradle-dummy-app:3.1.0-SNAPSHOT -Penvironment=int
obtemos la configuración del entorno int dentro de ${buildDir}/deploy/envConfig
.
Siguientes pasos
A partir de aquí tenemos varias opciones para continuar, según el entorno tecnológico.
Por un lado se podrían implementar las acciones del despliegue arriba descrito con un plugin de Gradle desarrollado a medida.
Por otro lado se podría integrar con otras tecnologías de automatización como por ejemplo Ansible.
En el caso de querer utilizar Ansible, podríamos crear un proyecto de configuración de entorno como el com.ats.gradle.dummy:gradle-dummy-env-int:2.3.0-SNAPSHOT conteniendo un Ansible Playbook.
Para esta integración con Ansible os recomiendo la lectura del artículo de Manuel Valle en su post Overwrite de variables hash globales por variables hash de entorno en Ansible.