【DEVOPS】最佳实践践行之“只生成一次制品”

本文意在对持续交付最佳实践之一的"只生成一次制品"进行思考和践行,提供Java侧的解决思路。(虽然这里是以Java为例,但正如标题所言,其思路是通用的)

1. 前言

对于Java应用开发,针对不同环境下的不同配置,最容易找到的解决方案通常都是借助Maven等提供的profile特性来在编译时将其固定在制品包中。

以上方法看似解决了不同环境下配置不同的需求,但无疑存在着不少隐忧:

  1. 看似是对同一份源码进行的打包,但实际生产过程中没有人敢100%保证在不同时间点打出来的两个包只有配置上的差异。
  2. 仅仅因为些许配置上的差异导致的重复打包,制品间存在着大量的重复。而在软件领域,重复就是最大的问题。

随着对于持续交付中相关理念的了解,笔者结合查找到的相关资料,汇总整理了一套通用的解决方案。

2. 解决方案选择

在正式开始介绍解决方案前,让我们针对不同的场景来整理下相应的应对方案(仅为一家之言,欢迎指正):

  1. 少量的配置项差异,使用替换的方式来完成。 例如编写自定义脚本实现指定配置项的替换(该自定义脚本直接存放在项目的版本库中,脚本的编写由各个项目组成员完成。部署流水线(例如Ansible或Jenkins)只负责按照约定好的传入参数顺序和意义,在约定的时机回调脚本即可)。
  2. 数量比较多的配置项,推荐按profile划分文件夹结构,并将原本的配置文件读取路径中增加一个关于profile的占位符。形如:
    ├─src
    │  ├─main
    │  │  ├─java
    │  │  │  ├─...                                   // Java业务代码
    │  │  └─resources
    │  │      └─profiles                             // 根据不同的环境配置好不同的配置项
    │  │          ├─dev
    │  │          │      application.properties
    │  │          │      db.properties
    │  │          │
    │  │          ├─prod
    │  │          │      application.properties
    │  │          │      db.properties
    │  │          │
    │  │          └─test
    │  │                  application.properties
    │  │                  db.properties	
    
  3. 以上方法适合于对现有系统的改造,对于全新的项目,则推荐进行完全的二进制应用包和运行时配置剥离,通过系统环境变量(例如 {PROJECT_NAME}_CONFIG_DIR )设置配置文件所在根目录等方式进行关联,然后在系统中读取相关配置项的时候要求按照规范,例如必须使用Spring方式,或者指定的工具类(工具类中进行profile判断)读取配置项。(此方案参考自《精通Spring4.x - 企业应用开发实战》- 18.11 如何规划便于部署的Web项目属性文件)。
  4. 使用配置中心。

3. 解决方案落地

对于现在的大部分Java Web应用来说,十有八九都是基于Spring的,笔者所在的公司也不例外,因此本文接下来的部分将主要以Spring框架作为基础进行讲解。

针对以上列举的方案清单,我们挑选出部分给出实现示例:

  1. 自定义脚本实现少量配置项部署时自动替换。
  2. 配置文件按profile文件夹分开存储。
3.1 自定义脚本实现部署时自动替换

准备阶段:

  1. Maven打包时候,确保profileImpl.ps1和最终生成的 JAR/WAR 同目录。
    在这里插入图片描述
  2. profileImpl.ps1脚本内容:
    <#
      $args[0] 当前profile
    #>
    
    cd $PSScriptRoot
    $profile = $args[0]
    
    # 抽取profiles文件夹
    jar -xf .\xxx-yy.jar profiles/
    # 抽取需要进行替换的文件
    jar -xf .\xxx-yy.jar static/i18n/yy_zh-CN.properties
    
    
    <#  至此, 文件夹结构如下:
        profiles/
        static/
          i18n/
            wwsb_zh-CN.properties
        xxx-yy.jar
    #>
    
    $profileProps =  convertfrom-stringdata (get-content ("./profiles/{0}.properties" -f $profile) -raw)
    Write-Host $profileProps['api_url']
    
    # 替换
    $TEMP_FILE = "static/i18n/yy_zh-CN.properties-temp"
    Get-Content  static/i18n/yy_zh-CN.properties  -Encoding utf8  | %{if($_ -match '^api_url=(?<num2>.*)$'){ ($_ -replace $Matches.num2, $profileProps['api_url'])  | Out-File -Encoding utf8 -Append $TEMP_FILE } else { $_ | Out-File -Encoding utf8 -Append $TEMP_FILE } }
    mv $TEMP_FILE static/i18n/yy_zh-CN.properties -Force
    
    # 写回JAR
    jar -uf .\xxx-yy.jar static/i18n/yy_zh-CN.properties
    
    # 还原现场
    rm -Force -Recurse profiles/
    rm -Force -Recurse static/
    

部署阶段:

  1. Ansible部署脚本。

    ### Ansible脚本, 这里只列出与自定义脚本相关的部分
    ---
    - hosts: "{{ TARGET  | default(windows_80) }}"
        - name: prepare
          ...
        - name: check if custom ps1 script exist
          win_stat:
            path: "{{WORKPATH}}/profileImpl.ps1"
          register: file_info
        - name: execute custom ps1 script when exist
          win_command: "powershell.exe -ExecutionPolicy ByPass -File {{WORKPATH}}/profileImpl.ps1 {{PROFILE_INNER}} >> {{WORKPATH}}/profileImpl.log"
          when: file_info.stat.exists
        - name: deploy app
          ...  
    
3.2 配置文件按profile文件夹分开存储

正如上面的解决方案清单中所述,本方案适合环境相关的配置项较多的场景。

这里我们就以常见的SpringMVC框架为例,陈述下其中的实现思路。

  1. 首先是配置文件的存储布局,上面已经展示了,这里再重复一次:
	├─src
	│  ├─main
	│  │  ├─java
	│  │  │  ├─...                                   // Java业务代码
	│  │  └─resources
	│  │      │  common.properties                  // 与环境无关的通用配置
	│  │      │
	│  │      └─profiles                            // 根据不同的环境配置好不同的配置项                          
	│  │          ├─dev
	│  │          │      application.properties
	│  │          │      db.properties
	│  │          │
	│  │          ├─prod
	│  │          │      application.properties
	│  │          │      db.properties
	│  │          │
	│  │          └─test
	│  │                  application.properties
	│  │                  db.properties	
	│  │                  db.properties	
  1. 接着是应用中的配置读取,在SpringMVC中我们通常采用的是XML配置方式。打开项目中相应的spring和springmvc配置文件最底部配置如下beans:

    <!-- 开发环境配置文件 -->
    <beans profile="dev">
        <context:property-placeholder
                location="classpath*:*.properties, classpath*:profiles/dev/*.properties"/>
    </beans>
    
    <!-- 测试环境配置文件 -->
    <beans profile="test">
        <context:property-placeholder
                location="classpath*:*.properties, classpath*:profiles/test/*.properties"/>
    </beans>
    
    <!-- 生产环境配置文件 -->
    <beans profile="prod">
        <context:property-placeholder
                location="classpath*:*.properties, classpath*:profiles/prod/*.properties"/>
    </beans>
    
  2. 最后一步就是告知Spring应该加载哪个profile了。相关的配置项为spring.profiles.active 。所以我们可以通过三种方式达到我们的目的:
    a. 直接修改 web.xml文件;
    b. 实现Servlet3.x中的ServletContainerInitializer接口。(在SpringMVC中可转而借助实现WebApplicationInitializer接口);
    c. 设置JVM参数。

    综合考虑之下我们决定暂时采用第一种方式来达到目的。相应的Ansible部署脚本部分如下:

        - name: configure profile for spring application
          win_lineinfile:
            path: "{{TOMCAT_INSTALL_WIN_DIR}}/apache-tomcat-{{TOMCAT_VERSION}}/webapps/{{PROJECT_NAME_INNER}}/WEB-INF/web.xml"
            state: present
            regex: '^.*<context-param><param-name>spring.profiles.active</param-name><param-value>\w+</param-value>'
            line: '    <context-param><param-name>spring.profiles.active</param-name><param-value>{{PROFILE_INNER}}</param-value>'
    
  3. 以上第2,3步,在SpringBoot的实现中相对就简单很多了。只需要一个配置:

    spring:
      config:
        additional-location: classpath:/profiles/${spring.profiles.active}/   # 指定配置文件的额外位置    (和spring.config.location配置项冲突, 源码参考 ConfigFileApplicationListener.getSearchLocations() )
    

4. 最后

以上解决方案即是目前笔者对于"持续交付"最佳实践——"只生成一次制品"的理解。

稍微深刻一些就可以看出来:所有方案的目的都是为了实现制品与配置分离,更具体一点是将配置项与配置值分离。即部署脚本只存放配置项,配置值应该由类似于配置中心的地方提供。

所以综合来看,笔者更推荐最开始列举的方案清单中的"基于系统变量约定配置文件所在根目录"的方式,这种简约版的配置中心方式可以彻底剥离应用与配置,最大化地避免应用更新对于配置的影响,减轻运维人员的心智负担

5. Links

  1. 《持续交付2.0》
  2. 《持续交付36讲》 第18课 - 如何做好容器镜像的个性化及合规检查?
  3. 《Jenkins2.x》第18.3.2章节
  4. Office Site - Spring Boot - Externalized Configuration
  5. SpringBoot2.X打包——一站式教程之分离配置和依赖
  6. springMVC的多环境配置_基于springprofile
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值