
# 如何将Java或者Tomcat应用打包成压缩包用于虚拟机部署方式部署组件？
使用虚拟机部署方式部署组件时，ServiceStage支持将Java或者Tomcat应用打包成zip或者tar.gz压缩包用于部署。
#### 应用压缩包内目录总体结构说明
- Java应用压缩包示例：<https://github.com/servicestage-demo/example/tree/master/servicestage-vm-demo/demoJavaExample.zip>
- Tomcat应用压缩包示例：<https://github.com/servicestage-demo/example/tree/master/servicestage-vm-demo/demoTomcatExample.zip>
以Java应用压缩包demoJavaExample.zip为例，应用压缩包内目录总体结构说明如下：
```
demoJavaExample/
│
├── scripts/
│   │      ├── pre-install.sh
│   │      ├── pre-stop.sh
│   │      ├── stop.sh
│   │      ├── uninstall.sh
│   │      ├── install.sh
│   │      ├── start.sh
│   │      ├── check.sh
│   │      ├── post-start.sh
├── packages/
│   │      ├── weather-1.0.0.jar
├── config/
│   │      ├── system.cfg
├── appspec.yml
```
**压缩包名前缀必须和解压后的文件目录名一致**。例如压缩包名为demoJavaExample.zip，解压后文件目录必须为demoJavaExample。
应用压缩包内各个目录及文件的作用说明如下：
- [scripts]：必选目录，存储的是应用各个生命周期执行的脚本文件。
 
- [packages]：必选目录，存储的是应用的jar包或者war包。
- [config]：必选目录，存储应用的配置信息。Java应用，存储的是system.cfg文件；Tomcat应用，存储的是system.cfg、logging.properties、server.xml文件。
- [appspec.yml]：必选文件，记录了生命周期的定义，也可以指定健康检查等信息。
 #### appspec.yml文件说明
如下所示，appspec.yml文件定义了整个部署的流程以及部署过程中使用到的环境变量和健康检查等内容。
```
spec:
  # 应用运行自定义用户
  deps:
    - name: "@os/linux/user@1.0"
      user: www
      group: www
      home: /home/www
  # 直接引入应用lifecycle脚本环境变量
  env:
  - name: APP_ENV
    value: "{{app.env}}"
  # 以文件形式引入保存于/opt/application/${appName}/${appVersion}/${instanceId}/servicestage-vmapp/application.conf的应用lifecycle脚本环境变量
  value:
  - name: APP_VALUE
    value: "{{app.value}}"
  # 应用健康检查
  probes:
    # 接口健康检查
    # health:
    #   exec:
    #     method: GET
    #     request: http://127.0.0.1:8080/healthcheck
    #     timeout: 5
    # 脚本命令健康检查
    liveness:
      exec:
        command:
          - ps -ef | grep ${APP_HOME}/apache-tomcat-*/bin/bootstrap.jar | grep -v grep   # 目前只支持APP_HOME可以取到环境变量
        timeout: 300
        runas: www
    retries: 10
    interval: 6
  # 应用lifecycle脚本
  lifecycle:
    pre-install:
      - command: scripts/pre-install.sh
        timeout: 300
    install:
      - command: scripts/install.sh
        timeout: 300
    check:
      - command: scripts/check.sh
        timeout: 300
        runas: www
    start:
      - command: scripts/start.sh
        timeout: 300
        runas: www
    post-start:
      - command: scripts/post-start.sh
        timeout: 300
        runas: www
    pre-stop:
      - command: scripts/pre-stop.sh
        timeout: 300
        runas: www
    stop:
      - command: scripts/stop.sh
        timeout: 300
        runas: www
    uninstall:
      - command: scripts/uninstall.sh
        timeout: 300
```
- deps : 此字段定义了应用运行自定义用户，建议参考如下示例固定。
  ```
  deps:
      - name: "@os/linux/user@1.0"
        user: www
        group: www
        home: /home/www
  ```
  
- env：此字段记录了各脚本运行时的环境变量，在脚本运行时可以直接通过环境引入。
- value：此字段记录的值会保存在${APP_HOME}/servicestage-vmapp/application.conf中。如果需要脚本中使用这些值，可以在脚本开始执行如下命令：
  ```
  #!/bin/bash 
    . ${APP_HOME}/servicestage-vmapp/application.conf
  # dosomething
  ```
  
- probes：健康检查方式，可以是health、liveness。两种健康检查方式同时使用时，需要两种方式返回状态都正常，才能表明组件处于存活状态；只使用其中一种健康检查方式时，只需要使用的方式返回状态正常，就表明组件处于存活状态；两种健康检查方式都不使用时，除了停止、删除组件的场景，其余场景默认组件存活。
- health：访问健康检查接口，查看组件状态。接口返回状态为200到400（不包括400）之间的状态码时，表明组件处于存活状态。
  - method：查看组件状态时访问接口使用的方法，支持HTTP、HTTPS。
  
  - request：访问的地址。
  
  - body：访问的请求体。
  
  - timeout：超时时间，单位为秒。
   
- liveness：执行指定命令，查看组件状态。命令执行无报错且返回不为空时，表明组件处于存活状态。
  - command：查看组件状态使用的命令。
  
  - runas：执行查看组件状态命令时的Linux用户。
  
  - timeout：命令执行超时时间，单位为秒。
   
- retries：健康检查的重试次数。应用启动时生效，超过此次数应用仍未启动时，认为组件启动失败。默认重试次数为10次，支持自定义。
- interval：健康检查方式的重试间隔。本次重试失败后，距离下次重试开始的时间间隔为：已重试次数\*默认重试间隔。默认重试间隔为6s，支持自定义。
- lifecycle：各个生命周期执行的脚本或者命令。
  - command：指定执行的脚本文件。必须是一个文件相对于${APP_HOME}文件夹所在的相对路径。
  
  - timeout：超时时间，单位为秒。
  
  - runas：执行的用户身份。
   
生命周期执行顺序：
- 部署的顺序：ServiceStage安装技术栈+pre-install.sh ---\>install.sh ---\>start.sh---\>check.sh---\>post-start.sh
- 升级/回滚的顺序：ServiceStage安装技术栈+pre-install.sh ---\>install.sh---\>pre-stop.sh---\>stop.sh---\>uninstall.sh+ServiceStage卸载技术栈---\>start.sh---\>check.sh---\>post-start.sh
- 删除的顺序：pre-stop.sh---\>stop.sh---\>uninstall.sh+ServiceStage卸载技术栈
 
 #### config说明
- system.cfg文件
  1. 设置config参数 在[使用虚拟机部署方式创建并部署组件](https://support.huaweicloud.com/usermanual-servicestage/servicestage_03_0267.html)时，您可以参考[添加配置项](https://support.huaweicloud.com/usermanual-servicestage/servicestage_03_0397.html)设置一些配置项，ServiceStage会将设置的配置项以键值对的形式存储在${APP_HOME}/config/system.cfg文件中。您也可以预置一些配置参数在config里面，格式参考示例如下：
     ```
     # system.cfg文件中存储内容的格式
     key1=value1
     key2=value2
     ```
     
  
  2. 引用config参数 引用config参数的方法示例如下：
     ```
     #!/bin/bash 
       . ${APP_HOME}/config/system.cfg 
       . ${APP_HOME}/config/user_config.cfg 
       echo ${key1}
     ```
     
   
- server.xml文件 请参考[tomcat的server.xml说明]。
  
- logging.properties文件 请参考[tomcat的logging.properties说明]。
  
 
 #### packages目录说明
您需要将执行的jar包、war包等内容保存在此文件夹中，在脚本中使用命令进行执行。
 #### scripts目录说明
此文件夹用于存储应用各个生命周期执行的脚本。
#### 系统默认配置
系统默认配置均在${APP_HOME}/servicestage-vmapp/application.conf文件中定义，并不会直接出现在环境变量中。application.conf文件中定义的环境变量需要在脚本中增加如下命令后才能引用。
```
. ${APP_HOME}/servicestage-vmapp/application.conf
```
可直接引用的脚本环境变量包括APP_HOME、在appspec.yml中指定的环境变量和[添加组件环境变量](https://support.huaweicloud.com/usermanual-servicestage/servicestage_03_0397.html)时指定的环境变量。
文件内容示例如下：
```
export LOG_PATH=/var/log/application/zqb-4-vm-wqd-2-7f6fbc/3c719644-f9f5-46b4-a06a-61fcf163e5b5
export APP_HOME=/opt/application/zqb-4-vm-wqd-2-7f6fbc/2023.1207.11314/3c719644-f9f5-46b4-a06a-61fcf163e5b5
export TOMCAT_STACK_HOME=/opt/application/zqb-4-vm-wqd-2-7f6fbc/2023.1207.11314/3c719644-f9f5-46b4-a06a-61fcf163e5b5/apache-tomcat-8.5.82
export JRE_STACK_HOME=/opt/application/zqb-4-vm-wqd-2-7f6fbc/2023.1207.11314/3c719644-f9f5-46b4-a06a-61fcf163e5b5/jre1.8
export APP_VALUE="{{app.value}}"
export APP_USER=www
export APP_GROUP=www
```
- LOG_PATH：日志记录的地址，需要统一将应用日志打印到LOG_PATH下的日志文件中，命名为{lifecycle}_app.log，方便后续的日志上报。
- APP_HOME：记录了当前应用运行的环境。
- TOMCAT_STACK_HOME：Tomcat的主目录。
- JRE_STACK_HOME：Jre的主目录。
- APP_VALUE：在appspec.yml中指定的环境变量。
- APP_USER：文件所属的用户。建议固定为www，也支持自定义用户。
- APP_GROUP：文件的属组。建议固定为www，也支持自定义用户属组。
如果自定义了用户及其属组，需要执行如下命令赋予该自定义用户运行脚本的权限：
```
sudo -- /bin/bash -c "chmod -R 755 ${APP_HOME}/scripts/* ${LOG_PATH};chown -R ${APP_USER}:${APP_GROUP} ${APP_HOME}/scripts/start.sh;chown -R ${APP_USER}:${APP_GROUP} ${APP_HOME}/scripts/stop.sh;chown -R ${APP_USER}:${APP_GROUP} ${LOG_PATH} ${JRE_STACK_HOME}/"
```
#### 脚本编写说明
Servicestage在执行脚本时的目录结构如下所示：
```
APP_HOME/
│
├── scripts/
│   │      ├── pre-install.sh
│   │      ├── pre-stop.sh
│   │      ├── stop.sh
│   │      ├── uninstall.sh
│   │      ├── install.sh
│   │      ├── start.sh
│   │      ├── check.sh
│   │      ├── post-start.sh
│   ├──packages/
│   │      ├── my-app.jar/my-app.war
├── config/
│   │      ├── system.cfg
├── servicestage-vmapp/
│   │      ├── application.conf
├── jre1.8
├── apache-tomcat-8.5.82
```
- 在编写脚本时，可以先进入${APP_HOME}目录，获取其他文件的位置。
- 若想获取jre或者tomcat的位置，可以在脚本开始位置执行。${APP_HOME}/servicestage-vmapp/application.conf加载环境变量，再通过${JRE_STACK_HOME}或${TOMCAT_STACK_HOME}引用获取。
- Servicestage会将${LOG_PATH}目录下的日志上报，因此需要统一将应用日志打印到${LOG_PATH}目录下的日志文件中，命名为{lifecycle}_app.log，方便后续日志上报。
- 建议在使用时，脚本都是使用如下几行作为开头，用于获取环境变量、用户配置、指定技术栈HOME、指定日志输出路径、指定当前执行地址等。
  ```
  #!/bin/bash
  # 获取用户参数、指定环境变量
  . ${APP_HOME}/config/system.cfg
  . ${APP_HOME}/servicestage-vmapp/application.conf
  # 指定技术栈HOME
  JRE_HOME=${JRE_STACK_HOME}
  TOMCAT_HOME=${TOMCAT_STACK_HOME}
  #指定日志输出地址
  installLog="${LOG_PATH}/install_app.log"
  #打印日志使用方法
  function writeLog()
  {
      msg="$1\n"
      printf "[`date '+%Y-%m-%d %H:%M:%S'`] $msg" | sudo tee -a ${installLog};
  }
  writeLog "-----begin!------"
  ```
  
- 如果使用自定义用户运行脚本，需要将stop.sh脚本中如下语句中的www运行用户名称替换为您的自定义运行用户名称。 替换前：
  ```
  ps -u www -ef|grep -v grep|grep ${JRE_HOME}/bin/java|awk '{print $2}
  ```
  替换后：
  ```
  ps -u {您的自定义运行用户} -ef|grep -v grep|grep ${JRE_HOME}/bin/java|awk '{print $2}
  ```
  例如：
  ```
  ps -u app-user -ef|grep -v grep|grep ${JRE_HOME}/bin/java|awk '{print $2}
  ```
  
 #### tomcat的server.xml说明
- 在创建tomcat组件时可以提前准备server.xml，将server.xml复制到tomcat的目录下（这一步骤建议在install生命周期中执行）。
  ```
  #!/bin/bash
  . ${APP_HOME}/servicestage-vmapp/application.conf 
  cp ${APP_HOME}/conf/server.xml ${TOMCAT_STACK_HOME}/conf/server.xml
  ```
  

- ServiceStage在${TOMCAT_STACK_HOME}/conf/server.xml有一份预置的server.xml文件，其中有四个占位符，使用时需要将其正确的替换，可以参考如下代码：
  ```
  #!/bin/bash
  . ${APP_HOME}/servicestage-vmapp/application.conf
  function replacePara()
  {
      sWord=$1
      dWord=$2
      theFile=$3
      if [[ "$sWord" == "" || "$theFile" == "" ]]
      then
          writeLog "[ERROR] ReplacePara has empty param, \$1:$sWord, \$3:$theFile"
          return 1
      fi
      if [[ ! -f ${theFile} ]]
      then
          writeLog "[ERROR] File $theFile does not exist."
          return 1
      fi
      count=`grep -c "$sWord" ${theFile}`
      if [[ "${count}" == "0" ]];then
          return 0
      fi
      sed "s#$sWord#${dWord}#g" ${theFile} > ${theFile}.temp
      if [[ "$?" != "0" ]]
      then
          writeLog "[ERROR] Sed command in replacePara error."
          return 1
      fi
      mv -f ${theFile}.temp ${theFile}
  }
  replacePara "@{LOG_FILE_PATH_APP}" "${LOG_FILE_PATH_APP}" ${TOMCAT_HOME}/conf/server.xml
  replacePara "@{http_port}" "${instance_port_port}" ${TOMCAT_HOME}/conf/server.xml
  replacePara "@{server_port}" "${server_port}" ${TOMCAT_HOME}/conf/server.xml
  replacePara "@{APP_PACKAGE_NAME}" "examples.war" ${TOMCAT_HOME}/conf/server.xml
  ```
  - @{server_port}：tomcat服务器的端口号。
  
  - @{APP_PACKAGE_NAME}：war包相对于${TOMCAT_STACK_HOME}目录的位置。
  
  - @{http_port}： tomcat的Connector的端口号。
  
  - @{LOG_FILE_PATH_APP}：tomcat日志打印的路径。
    
 
 #### tomcat的logging.properties说明
您需要自行准备logging.properties，将其复制到tomcat的目录下，复制代码示例如下：
```
#!/bin/bash
. ${APP_HOME}/servicestage-vmapp/application.conf 
cp ${APP_HOME}/conf/logging.properties ${TOMCAT_STACK_HOME}/conf/logging.properties
```
logging.properties可以参考如下内容，使用时将@{LOG_FILE_PATH_APP}替换为真正的日志目录。
```
handlers = 1catalina.org.apache.juli.AsyncFileHandler, 2localhost.org.apache.juli.AsyncFileHandler, 3manager.org.apache.juli.AsyncFileHandler, 4host-manager.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
.handlers = 1catalina.org.apache.juli.AsyncFileHandler, java.util.logging.ConsoleHandler
1catalina.org.apache.juli.AsyncFileHandler.level = FINE
1catalina.org.apache.juli.AsyncFileHandler.directory = @{LOG_FILE_PATH_APP}
1catalina.org.apache.juli.AsyncFileHandler.prefix = catalina.
2localhost.org.apache.juli.AsyncFileHandler.level = FINE
2localhost.org.apache.juli.AsyncFileHandler.directory = @{LOG_FILE_PATH_APP}
2localhost.org.apache.juli.AsyncFileHandler.prefix = localhost.
3manager.org.apache.juli.AsyncFileHandler.level = FINE
3manager.org.apache.juli.AsyncFileHandler.directory = @{LOG_FILE_PATH_APP}
3manager.org.apache.juli.AsyncFileHandler.prefix = manager.
4host-manager.org.apache.juli.AsyncFileHandler.level = FINE
4host-manager.org.apache.juli.AsyncFileHandler.directory = @{LOG_FILE_PATH_APP}
4host-manager.org.apache.juli.AsyncFileHandler.prefix = host-manager.
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = org.apache.juli.OneLineFormatter
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.AsyncFileHandler
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.AsyncFileHandler
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].handlers = 2localhost.org.apache.juli.AsyncFileHandler
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/manager].handlers = 3manager.org.apache.juli.AsyncFileHandler
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].level = INFO
org.apache.catalina.core.ContainerBase.[Catalina].[localhost].[/host-manager].handlers = 4host-manager.org.apache.juli.AsyncFileHandler
```
#### FAQ
- 如何以root账户运行脚本命令？
  1. 将要以root账户运行的内容编写成一个脚本root-install.sh。
  
  2. 在真正要执行的install.sh执行如下语句，其中${ROOT_PASSWORD}为root账号的密码，-c后面的双引号中为执行的命令。切换用户后会丢失环境变量，可以在sh前加上所需要的环境变量。
     ```
     echo "${ROOT_PASSWORD}" | su - root -c "APP_HOME=${APP_HOME} sh root-install.sh"
     ```
     
  
  3. 若执行上述语句发生"su: Permission denied"报错，找到/etc/pam.d/su文件，将" auth required pam_wheel.so use_uid"这一句注释掉。
   
 
