CI/CD持续集成/持续部署实践

1. 概述

本文是对CI/CD具体流程的一种实践,仅代表个人观点。在阅读本文前掌握以下内容,更方便理解。

1. 对K8S有基本了解,部署过具体应用;
2. 使用过Helm部署K8S应用,了解Helm基本原理;
3. 熟练使用Jenkins,理解流水线任务和Jenkins公共库的概念;
4. 熟悉Harbor、GitLab和sonarQube相关工具;

本文目标如下:

  1. 实现微服务应用从提交代码到部署应用到K8S的全自动化流程,其中包括:代码下载、静态分析、单元测试、编译发布、环境切换和安装部署等工作,整个过程不需要人工干预;
  2. 借助Jenkins Library公共库,对部署过程的步骤进行抽象,不需要每个项目都写Jenkinsfile文件(如果特殊情况也可以定义自己的);
  3. 借助Kubectl Config工具,对K8S集群环境访问进行动态切换,实现不同集群环境部署。
  4. 借助Helm Chart自定义Chart功能,制定标准的K8S发布所需资源模板Chart,开发人员无需关注部署的K8S YAML全部语法,直接指定values.yaml文件中需要的资源即可;

另外,除了在部署流程的标准化、自动化之外,还需要考虑面向微服务应用开发人员和运维人员的配置人性化问题。例如:让一个开发人员去了解复杂的K8S配置,理论上不太实际,同样的运维人员对部署应用所需的条件(例如:CPU和内存)等也不能同等对待,需要开发人员配合给出。实现将运维和研发的关注点分离。

CI/CD具体流程如下:


CI/CD流程

如上图,为CI/CD整个发布流程,该方案特点:

1. CI/CD发起支持手工构建和GitLab的WebHook构建两种启动方式;
2. 支持Maven结构的单模块和多模块微服务应用的发布;

具体流程说明如下:

1. 由开发人员PUSH代码或手工在Jenkins平台触发构建;
2. 开发人员提交代码后,GitLab会向Jenkins发起WebHook调用;
3. Jenkins平台通过指定的"流水线"配置的GitLab上的Jenkins Library地址,下载Jenkinsfile文件;
4. JenkinsFile文件中通过"@Library"命令加载在Jenkins配置的公共库,并调用公共库里面的"build"方法;
5. 从GitLab上下载项目源文件;
6. 通过maven插件sonarqube对源代码进行静态分析,并将结果上传到SonarQube服务器上;
7. 开始单元测试;
8. 通过Maven插件jib打包镜像并PUSH到Harbor仓库;
9. 通过Kubectl Config工具切换要部署到的K8S平台;
10. 通过Helm install向指定的K8S发起创建POD请求;
11. 最后,K8S接收到Helm的安装请求后,从Harbor中下载Docker镜像和Helm Chart部署系统。

2. 微服务应用

微服务应用以容器的方式运行在K8S Pod上,在Kubernetes集群中,Pod是所有业务类型的基础,也是K8S管理的最小单位级,它是一个或多个容器的组合。这些容器共享存储、网络和命名空间,以及如何运行的规范。在Pod中,所有容器都被同一安排和调度,并运行在共享的上下文中。对于具体应用而言,Pod是它们的逻辑主机,Pod包含业务相关的多个应用容器。
微服务应用部署结构如下:



如上图所示,微服务应用部署包括以下三个容器:

1. Skywalking-Agent-Sidecar容器:initContainers容器,该容器在POD启动前执行,执行完成后立即停止,主要目的是将Skywalking Agent所需要的
   文件拷贝进容器组中,其中连接Skywalking服务器的配置文件是通过ConfigMap方式注入进容器;
2. 微服务应用容器:该容器为主容器,运行了程序员定义的微服务程序(采用java -cp方式运行,运行目录为/app,包括classes、libs和resources
   三部分)。启动时采用了-javaagent代理Skywalking运行,用于收集链路日志记录,同时内置Consul服务发现和服务配置,通过访问
   Consul-Agent-SideCar与Consul Server进行通信;
3. Consul-Agent-SideCar容器:该容器伴随微服务主容器一起运行,生命周期与主容器一样,主要目的是在Consul Server服务器上建立一个Consul 
   Client,用于在微服务应用与Consul Server之间转发请求,主容器微服务只需要通过127.0.0.1:8500进行通信,而不用关心Consul Server的部署IP
   和端口;Consul Agent连接服务器的配置信息是通过ConfiMap注入进来的;

2.1 Skywalking-Agent-Sidecar镜像

skywalking-agent-sidecar镜像包括skywalking agent所需的文件,如下:

在微服务应用容器运行时,作为initContainers容器,项目Pod启动时负责拷贝文件到指定位置,供项目启动时调用,运行完成后会停止该容器。

2.2 Consul-Agent-Sidecar镜像

consul-agent-sidecar镜像是一个边车模式的镜像,与微服务应用容器生命周期保持一致,主要负责与远端的Consul Server通信,这样项目容器通过127.0.0.1:8500访问即可,不用关心Consul Server的IP地址和端口。


2.3 微服务应用镜像

微服务应用镜像是将开发代码打包成镜像,能够运行在K8S上。需要部署的项目都是采用Maven的文件结构,具体如下:


2.3.1 集成jib插件

微服务应用项目需要集成jib插件,jib无需Docker守护程序即可为Java应用程序构建优化的Docker和OCI映像-无需深入掌握Docker最佳实践。它可以作为Maven和Gradle的插件以及Java库使用。

1. 快速-快速部署您的更改。Jib将您的应用程序分成多个层,将依赖项与类分开。现在,您不必等待Docker重建整个Java应用程序-只需部署已更改的层。
2. 可重现-用相同的内容重建容器映像始终会生成相同的映像。永远不要再次触发不必要的更新。
3. 无守护程序-减少CLI依赖性。从Maven或Gradle内部构建Docker映像,然后推送到您选择的任何仓库(例如:Harbor)。无需再编写Dockerfile
   并调用docker build / push。

平台已经提供,只需要在pom.xml中加入如下配置即可:

<properties>
  <!-- 去掉发布后镜像的环境前缀 -->
  <docker.image.project.version.prefix></docker.image.project.version.prefix>
  <!-- 自定义镜像启动主类,默认为com.egrand.EgrandCloudApplication -->
  <docker.image.project.main.class>com.egrand.EgrandCloudXXXApplication</docker.image.project.main.class>
</properties>
<build>
  <plugins>
    <!-- 引入jib插件 -->
      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
      </plugin>
   </plugins>
</build>
2.3.2 集成sonarQube

Static Analysis采用sonarQube来实现,需要安装sonarQube服务。Sonar(SonarQube)是一个开源平台,用于管理源代码的质量。Sonar 不只是一个质量数据报告工具,更是代码质量管理平台。支持的语言包括:Java、PHP、C#、C、Cobol、PL/SQL、Flex 等。

1. 代码覆盖:通过单元测试,将会显示哪行代码被选中;
2. 改善编码规则;
3. 搜寻编码规则:按照名字,插件,激活级别和类别进行查询;
4. 项目搜寻:按照项目的名字进行查询;
5. 对比数据:比较同一张表中的任何测量的趋势;

进行代码静态分析时,需要准备以下条件:

  1. 安装sonar服务器端,这里不详细介绍;
  2. 生成Login Token;
  3. 修改maven的setting.xml文件,增加如下配置:
    <pluginGroups>
        <pluginGroup>org.sonarsource.scanner.maven</pluginGroup>
    </pluginGroups>
    <profiles>
        <profile>
          <id>sonar</id>
          <activation>
              <activeByDefault>true</activeByDefault>
          </activation>
          <properties>
              <!-- Optional URL to server. Default value is http://localhost:9000 -->
              <sonar.host.url>
                  http://127.0.0.1:9000
              </sonar.host.url>
              <sonar.login>
                  [上一步生成的Login Token]
              </sonar.login>
          </properties>
      </profile>
    </profiles>
    
    其中sonar.login是在sonar服务端生成的登录token。
  4. 访问sonar服务器,新建项目
    具体过程不介绍,但最终会生成一个”项目标识",需要配置到下一步骤的。
  5. 配置项目pomx.xml文件
    <properties>
        <sonar.projectKey>[在sonar服务器上新建项目的项目标识]</sonar.projectKey>
    </properties>
    
2.3.3 bootstrap.yml规范

微服务应用在通过Jenkins平台部署到K8S形成pod后,在pod中会采用边车模式运行Consul-Agent-Sidecar容器,微服务应用注册到Consul是采用localhost:8500的方式进行通信即可;
同时在Jenkins Library中运行jib,是默认采用"mvn clean compile jib:build -DsendCredentialsOverHttp=true -Dprofile.active=prod"来运行的,其中prod代表的是K8S线上环境(包括K8S测试环境和K8S正式环境等,不区分);
综上述,在配置bootstrap.yml中配置prod时一定保持如下:

server:
  port: 8888
spring:
  application:
    name: egrand-cloud-platform-demo
  profiles:
    active: @profile.active@
  cloud:
    consul:
      config:
        # 设置配置的基本文件夹,默认值 config 可以理解为配置文件所在的最外层文件夹
        prefix: platform
        # 配置环境分隔符,默认值 "," 和 default-context 配置项搭配
        # 例如应用 orderService 分别有环境 default、dev、test、prod
        # 只需在 config 文件夹下创建 orderService、orderService-dev、orderService-test、orderService-prod 文件夹即可
        profile-separator: '-'
        # 指定配置格式为 yaml
        format: YAML
# 本地开发环境
---
spring:
  profiles: dev
  cloud:
    consul:
      # consul注册中心的ip地址
      host: ${CONSUL_HOST:10.1.10.179}
      # consul注册中心端口
      port: ${CONSUL_PORT:8600}
      # 配置中心相关配置
      config:
        # 认证token
        acl-token: ${CONSUL_TOKEN:6983d4bf-87d6-591a-c3d4-dde5c05c4f28}
# 线上环境
---
spring:
  profiles: prod
  cloud:
    consul:
      # consul注册中心的ip地址
      host: localhost
      # consul注册中心端口
      port: 8500
      # 配置中心相关配置
      config:
        # 认证token
        acl-token: ${CONSUL_TOKEN}

4. Jenkins Library公共库

4.1 目标

Jenkins Library公共库目标是规范Jenkins发布步骤,将每个项目在构建中的步骤进行抽象,规范参数传递来实现不同项目的公共构建。用户可以在新建Jenkins任务时加载公共依赖,调用公共方法,达到重复利用的目的。

1. 规范化:对每个项目中的步骤进行抽象实现,同时规范化输入参数;
2. 统一化:如果有改动,改动这一处即可;

4.2 目录结构

Jenkins Library存储在GitLab上,具体目录结构如下:

egrand-cloud-jenkins-library
   |--- deploy
   |      |--- Jenkinsfile  // 公共的Jenkinsfile,项目在创建Jenkins任务时,引用该文件开始构建,该文件调用vars/build.groovy的build方法
   |--- vars
   |--- build.groovy  // 构建主文件,为Jenkinsfile提供调用方法build
   |--- gitlab.groovy  // 子流程步骤,供build.groovy调用,完成从GitLab上下载源代码
   |--- jib.groovy  // 子流程步骤,供build.groovy调用,完成maven插件jib构建,构建镜像并推送到Harbor中
   |--- kubecm.groovy  // 子流程步骤,供build.groovy调用,完成kubectl config环境切换和Helm uninstall/install操作,部署到K8S集群
   |--- sonar.groovy  // 子流程步骤,供build.groovy调用,完成静态代码分析,并推送到SonarQube服务器;

4.3 运行步骤

如上图,Jenkins Library存放在GitLab上,其中包括了公共的Jenkinsfile和其他脚本文件。

  • Jenkinsfile文件:
// 加载jenkins公共库
@Library('jenkins-library') _
/**
 * 构建参数,具体参数如下:
 * cloud             运行K8S的云节点,默认值为kubernetes,在Jenkins的"Configure Clouds(配置集群)"中配置
 * serviceAccount    K8S运行账号,默认是jenkins2,需要在K8S集群中分配账号信息
 * chart             Helm的自定义chart,默认为egrand/backend-deployment-chart
 * profileActive     Maven编译环境,默认为prod
 * gitUrl            Git源码地址
 * branches          Git源码分支,接收jenkins在任务中配置的branch参数值
 * credentialsId     Git认证ID,在jenkins中配置
 * imageTag          镜像版本,默认值是根据gitlab提交信息生成
 * preImageTag       上一镜像版本,从GitLab提交历史获取的
 * moduleDir         多模块运行目录(对于多模块项目才需要设置,单模块不需要)
 * deployToEnv       部署到的环境,包括dev:开发环境;prod:生成环境
 *                   注意:如果不填写该值,则自动根据branches字段值来判断
 *                   1. branches的值等"master"或者"refs/heads/master",则deployToEnv值为prod
 *                   2. branches的值等"dev"或者"refs/heads/dev",则deployToEnv值为dev
**/
build([
    gitUrl:"${gitUrl}",
    imageTag: "${gitCommitId}",
    preImageTag: "${gitPreCommitId}",
    branches: "${branch}",
    moduleDir: "${env.moduleDir}"
])

如上,Jenkinsfile定义了公共的脚本,在新建项目的Jenkins任务时可以引用,通过调用build方法,进入执行流程,以下是相关参数说明:

参数名称 说明
cloud 运行K8S的云节点,默认值为kubernetes,在Jenkins的"Configure Clouds(配置集群)"中配置
serviceAccount K8S运行账号,默认是jenkins2,需要在K8S集群中分配账号信息
gitUrl Git源码地址
branches Git源码分支,值包括:dev(部署到K8S的测试环境)、master(部署到K8S的正式环境)
credentialsId Git认证ID,在jenkins中配置
chart Helm的自定义chart,包括:egrand/backend-deployment-chart、egrand/foreend-deployment-chart
imageTag 镜像版本,默认值是根据gitlab提交信息生成,截取前7位,与项目中jib插件生成的镜像版本保持一致
preImageTag 上一次镜像版本,用于在部署时删除前一个部署,避免出现重复的pod
profileActive Maven编译环境,默认为prod
moduleDir 多模块运行目录(对于多模块项目才需要设置,单模块不需要)
deployToEnv 部署到的环境,包括dev:开发环境;prod:生成环境 如果不指定deployToEnv,则如下: 根据branches值自动生成发布环境 * master ---> 生产环境(k8s) * dev ---> 开发环境(k8s)
  • 执行脚本

执行脚本包括build.groovy、gitlab.groovy、sonar.groovy、jib.groovy和kubecm.groovy五个脚本,其中build.groovy作为脚本的进入口,同时定义了pipeline流水线。</pre>
脚本说明如下:

脚本 备注
build.groovy 构建主文件,为Jenkinsfile提供调用方法build
gitlab.groovy 子流程步骤,供build.groovy调用,完成从GitLab上下载源代码
sonar.groovy 子流程步骤,供build.groovy调用,完成静态代码分析,并推送到SonarQube服务器;
jib.groovy 子流程步骤,供build.groovy调用,完成maven插件jib构建,构建镜像并推送到Harbor中
kubecm.groovy 子流程步骤,供build.groovy调用,完成kubectl config环境切换和Helm uninstall/install操作,部署到K8S集群

5. Helm自定义Chart

5.1 概述

Helm是Kubernetes的包管理器,类似于Python的pip centos的yum,主要用来管理 Charts。Helm Chart是用来封装Kubernetes原生应用程序的一系列YAML文件。可以在你部署应用的时候自定义应用程序的一些Metadata,以便于应用程序的分发。

* 对于应用发布者而言,可以通过Helm打包应用、管理应用依赖关系、管理应用版本并发布应用到软件仓库。
* 对于使用者而言,使用Helm后不用需要编写复杂的应用部署文件,可以以简单的方式在Kubernetes上查找、安装、升级、回滚、卸载应用程序

5.2 介绍

backend-deployment-chart是基于Helm自定义的后端微服务发布chart,里面定义了K8S生成所需的所有资源和YAML定义,目的是简化和规范发布流程;

* 屏蔽K8S复杂的YAML配置文件,利用Helm的-f指定参数覆盖文件或通过--set命令覆盖参数,
  就能够基于backend-deployment-chart发布项目的前端文件;
* 提供Skywalking Ageng功能,记录调用链路日志给服务器端;
* 提供Consul Client边车模式,让后端微服务访问本地Consul Client,而不需要关系Consul Server在哪里;

backend-deployment-chart目录如下:

foreend-deployment-chart
   |---templates                                // 模板文件
   |      |---_helpers.tpl                      // 帮助模板,可以理解为定义公共的组合变量
   |      |---consulAgent-configMap.yaml        // Consul Agent配置字典文件
   |      |---deployment.yaml                   // k8s的deployment方式yaml定义文件,用于生成Deployment pod
   |      |---harbor-secret.yaml                // Harbor密文配置文件
   |      |---hpa.yaml                          // 水平动态扩展(暂时不用)
   |      |---ingress.yaml                      // ingress定义文件(内网采用NodePort方式,暂时不用)
   |      |---service.yaml                      // 服务YAML定义,用于暴露接口(目前采用NodePort方式)
   |      |---serviceaccount.yaml               // K8S帐户YAML定义,默认为default
   |      |---skywalking-configMap.yaml         // Skywalking Agent代理的配置字典文件
   |---Chart.yaml                               // 包含了该chart的描述。你可以从模板中访问它。
   |---values.yaml                              // 包含了chart的默认值 。这些值会在用户执行helm install 或 helm upgrade时被覆盖。
           

foreend-deployment-chart已经发布到harbor中,通过以下命令生成pod:

# 运行线上chart生成一个POD,默认的namespace为default
> helm install [pod名称] egrand/backend-deployment-chart
# 采用values.yaml复写参数运行线上的Chart并生成一个新的pod
> helm install -f values.yaml [pod名称] egrand/backend-deployment-chart

可以通过以下代码下载:

# 搜索镜像
> helm search repo backend
# 下载egrand源下的backend-deployment-chart并解压
> helm pull egrand/backend-deployment-chart --untar

5.3 部署约定

为规范后端微服务部署流程,制定的规定如下:

  • Skywalking初始化容器
    通过上面的架构图,可以知道,Skywalking的相关配置文件时通过初始化容器运行时拷贝进入主容器中的,其中Skywalking Agent连接Skywalking Server的配置文件是通过ConfigMap注入进来的,文件名为:agent.config,存储路径为:/usr/skywalking/agent/config/;
  • 微服务镜像规范
    建议采用jib打包Docker镜像,平台Maven中已内置插件,运行命令即可;
  • Consul Client运行约定
    一个微服务应用对应一个Consul Client,同生共死,相互之间通过127.0.0.1进行通信,其中Consul Client连接Consul Server的配置信息是通过ConfigMap注入进来的文件,文件名为:agent.json,存储路径为“/etc/consul/config/”;

5.4 部署

下面介绍如何通过覆盖参数配置来实现一个项目的微服务应用部署。

其实很简单,backend-deployment-chart已经帮我们定义好了部署需要的资源,
我们只需要覆盖对应默认参数即可,就像程序开发中调用一个已经写好的公共方法,只需要传入参数即可。
  • -f 覆盖默认参数部署
    在新的目录下(随便哪个目录)新建文件一个文件名为values.yaml文件,加入如下内容:

    # Consul Agent ConfigMap配置
    consulAgentConfigmap: |
      {
        "bind_addr": "0.0.0.0",
        "client_addr": "0.0.0.0",
        "datacenter": "egdPlatform",
        "primary_datacenter": "egdPlatform",
            "encrypt": "EXz7LFN8hpQ4id8EDYiFoQ==",
        "data_dir": "/consul/data",
        "acl": {
            "enabled": true,
            "default_policy": "deny",
            "enable_token_persistence": true,
            "tokens": {
                "agent": "b07df4e4-1183-5496-35fe-0ac6eb726d05"
            }
        },
        "log_level": "INFO",
        "log_file": "/consul/consul.log",
        "log_rotate_duration": "24h",
        "enable_syslog": false,
        "enable_debug": true,
        "connect":{
            "enabled": true
          },
            "ui": true,
        "start_join":[
            "consul-0.consul.kube-platform.svc.cluster.local",
            "consul-1.consul.kube-platform.svc.cluster.local",
            "consul-2.consul.kube-platform.svc.cluster.local"
          ]
      }
    # skywalking配置文件configMap
    skywalkingConfig: |
      agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
      collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:10.1.10.178:11800}
      logging.file_name=${SW_LOGGING_FILE_NAME:skywalking-api.log}
      logging.level=${SW_LOGGING_LEVEL:INFO}
      plugin.mount=${SW_MOUNT_FOLDERS:plugins,activations}
    #Consul SideCar镜像配置
    consulSideCarImage:
      #是否开启
      enabled: true
    # 初始化容器配置
    initContainer:
      #是否开启
      enabled: true
    #应用镜像配置
    image:
      #仓库地址
      repository: [微服务应用镜像地址]
      #镜像版本,默认为appVersion.
      tag: "[微服务应用镜像版本]"
    

    将第一步生成镜像的地址和版本填入,运行以下命令部署

    > helm install -f values.yaml demo egrand/backend-deployment-chart
    

    即可完成部署。

5.5 values.yaml参数详解

# Default values for backend-deployment-chart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

# Pod副本数量
replicaCount: 1
# 中文名称
displayName: "微服务应用"
# 部署到的命名空间
namespace: ""
# 所属层级(方便在Kuboard中查看,默认为svc)
#       svc:微服务层
#       gateway;网关层
#       cloud:中间件
#       web:展现层
layer: ""
# 名称(覆盖变量backend-deployment-chart.name的值,默认值为Chart.Name)
# backend-deployment-chart.name变量用于定义标签app.kubernetes.io/name的值,
# 形成app.kubernetes.io/name: backend-deployment-chart.name
# 利用该标签和app.kubernetes.io/instance: .Release.Name进行标签关联selector选择用
nameOverride: ""
# 发布全名称,定义Pod的名称(覆盖变量backend-deployment-chart.fullname的值,默认[Release.Name]-[Chart.Name])
fullnameOverride: ""
# pod注解
podAnnotations: {}
# 安全设置
podSecurityContext: {}
# fsGroup: 2000
# 节点选择
nodeSelector: {}
# 容忍设置
tolerations: []
# 亲和性设置
affinity: {}
# pod上定义共享存储卷列表
volumes: {}

#镜像拉取密文配置
imageCredentials:
  # 覆盖名称(与config是冲突的,如果填写该值,则在K8S namespace查找配置字典)
#  nameOverride: "harborsecret"
  config:
    # 镜像仓库地址
    registry: http://10.1.10.212:8082/
    # 镜像登录名
    username: admin
    # 镜像登录密码
    password: wiki2012

#Consul Agent代理配置字典
#consulAgentConfigMap:
#  # 覆盖名称(与config是冲突的,如果填写该值,则在K8S namespace查找配置字典)
#  nameOverride: "consul-agent"
#  # 配置字典配置信息
#  config:
#    agent.json: |
#      {
#        "bind_addr": "0.0.0.0",
#        "client_addr": "0.0.0.0",
#        "datacenter": "egdPlatform",
#        "primary_datacenter": "egdPlatform",
#            "encrypt": "EXz7LFN8hpQ4id8EDYiFoQ==",
#        "data_dir": "/consul/data",
#        "acl": {
#            "enabled": true,
#            "default_policy": "deny",
#            "enable_token_persistence": true,
#            "tokens": {
#                "agent": "b07df4e4-1183-5496-35fe-0ac6eb726d05"
#            }
#        },
#        "log_level": "INFO",
#        "log_file": "/consul/consul.log",
#        "log_rotate_duration": "24h",
#        "enable_syslog": false,
#        "enable_debug": true,
#        "connect":{
#            "enabled": true
#          },
#            "ui": true,
#        "start_join":[
#            "consul-0.consul.kube-platform.svc.cluster.local",
#            "consul-1.consul.kube-platform.svc.cluster.local",
#            "consul-2.consul.kube-platform.svc.cluster.local"
#          ]
#      }


# skywalking配置字典
#skywalkingAgentConfigMap:
#  # 覆盖名称(与key和value是冲突的,如果填写该值,则在K8S namespace查找配置字典)
#  nameOverride: "skywalking-config"
#  # 配置字典配置信息
#  config:
#    agent.config: |
#      agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
#      collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:10.1.10.178:11800}
#      logging.file_name=${SW_LOGGING_FILE_NAME:skywalking-api.log}
#      logging.level=${SW_LOGGING_LEVEL:INFO}
#      plugin.mount=${SW_MOUNT_FOLDERS:plugins,activations}

#应用镜像配置
image:
  #仓库地址
  repository: 10.1.10.212:8082/library/nginx
  #镜像版本,默认为appVersion.
  tag: "latest"
  #镜像拉去策略:Always(总是拉取 pull)、
  #            IfNotPresent(默认值,本地有则使用本地镜像,不拉取)、
  #            Never(只使用本地镜像,从不拉取)
  pullPolicy: Always
  #环境变量设置,采用name和value格式。
#  env:
#    - name: TZ
#      value: Asia/Shanghai
#    - name: spring.datasource.dynamic.datasource.master.url
#      value: >-
#        jdbc:mysql://10.1.10.188:3306/egrand-cloud-ky3h-cis?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8&connectTimeout=60000&socketTimeout=30000
#    - name: springfox.documentation.swagger.v2.host
#      value: >-
#        10.1.10.212/ky3h
  # 安全设置
  securityContext: {}
    # capabilities:
    #   drop:
    #   - ALL
    # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000
  # 资源配置
  resources: {}
    # We usually recommend not to specify default resources and to leave this as a conscious
    # choice for the user. This also increases chances charts run on environments with little
    # resources, such as Minikube. If you do want to specify resources, uncomment the following
    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
    # limits:
    #   cpu: 100m
    #   memory: 128Mi
    # requests:
  #   cpu: 100m
  #   memory: 128Mi
  #镜像挂载
  volumeMounts: {}
    #主机模式
#    - mountPath: /usr/local/skywalking/agent
#      mountPropagation: None
#      name: skywalking-agent
    #PVC模式
#    - name: atm-data
#        persistentVolumeClaim:
#          claimName: kube-yuncang

#Consul SideCar镜像配置
consulSideCarImage:
  #是否开启
  enabled: false
  #镜像名称
  name: "consul-agent-sidecar"
  #仓库地址
  repository: 10.1.10.212:8082/egrand-cloud-deploy/consul-agent-sidecar
  #镜像版本
  tag: "latest"
  #镜像拉去策略:Always(总是拉取 pull)、
  #            IfNotPresent(默认值,本地有则使用本地镜像,不拉取)、
  #            Never(只使用本地镜像,从不拉取)
  pullPolicy: IfNotPresent
  # 资源配置
  securityContext: {}
    # capabilities:
    #   drop:
    #   - ALL
    # readOnlyRootFilesystem: true
    # runAsNonRoot: true
    # runAsUser: 1000
  # 资源配置
  resources: {}
    # We usually recommend not to specify default resources and to leave this as a conscious
    # choice for the user. This also increases chances charts run on environments with little
    # resources, such as Minikube. If you do want to specify resources, uncomment the following
    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
    # limits:
    #   cpu: 100m
    #   memory: 128Mi
    # requests:
    #   cpu: 100m
    #   memory: 128Mi

# 初始化容器配置
initContainer:
  #是否开启
  enabled: false
  #镜像名称
  name: "skywalking-agent-sidecar"
  #仓库地址
  repository: 10.1.10.212:8082/egrand-cloud-deploy/skywalking-agent-sidecar
  #镜像版本
  tag: "latest"
  #镜像拉去策略:Always(总是拉取 pull)、
  #            IfNotPresent(默认值,本地有则使用本地镜像,不拉取)、
  #            Never(只使用本地镜像,从不拉取)
  pullPolicy: IfNotPresent
  # 资源配置
  securityContext: {}
    # capabilities:
    #   drop:
    #   - ALL
    # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000
  # 资源配置
  resources: {}
    # We usually recommend not to specify default resources and to leave this as a conscious
    # choice for the user. This also increases chances charts run on environments with little
    # resources, such as Minikube. If you do want to specify resources, uncomment the following
    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
    # limits:
    #   cpu: 100m
    #   memory: 128Mi
    # requests:
  #   cpu: 100m
  #   memory: 128Mi
  # 运行参数配置
  args:
    - '-c'
    - >-
      mkdir -p /skywalking/agent && cp -r /usr/skywalking/agent/* /skywalking/agent
  # 运行命令配置
  command:
    - sh

# 服务配置
service:
  # service的类型访问方式。一般为NodePort、ClusterIP、LoadBalancer
  type: NodePort
  # 内部端口
  ports: {}
#    - name: names
#      port: 80
#      protocol: TCP
#      targetPort: 80

# 帐户设置,create为false时,会自动设置帐户名为default
serviceAccount:
  # 是否创建帐户
  create: false
  # 帐户注解
  annotations: {}
  # 帐户名,不设置并且create为true,则帐户名为backend-deployment-chart.fullname
  # If not set and create is true, a name is generated using the fullname template
  name: ""

ingress:
  enabled: false
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chart-example.local
      paths:
      - path: /
        backend:
          serviceName: chart-example.local
          servicePort: 80
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chart-example.local

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80
以上参数都可以通过定义values.yaml进行复写,在安装时使用helm install -f values.yaml demo egrand/backend-deployment-chart覆盖默认参数。
5.5.1 微服务应用配置说明

微服务应用镜像是由开发人员将开发好的微服务上传到Harbor,在生产容器时会根据指定Harbor镜像地址自动下载并启动。在配置中通过以下配置声明微服务镜像:

#应用镜像配置
image:
  #仓库地址
  repository: 10.1.10.212:8082/library/nginx
  #镜像版本,默认为appVersion.
  tag: "latest"
  #镜像拉去策略:Always(总是拉取 pull)、
  #            IfNotPresent(默认值,本地有则使用本地镜像,不拉取)、
  #            Never(只使用本地镜像,从不拉取)
  pullPolicy: Always
  #环境变量设置,采用name和value格式。
#  env:
#    - name: TZ
#      value: Asia/Shanghai
#    - name: spring.datasource.dynamic.datasource.master.url
#      value: >-
#        jdbc:mysql://10.1.10.188:3306/egrand-cloud-ky3h-cis?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8&connectTimeout=60000&socketTimeout=30000
#    - name: springfox.documentation.swagger.v2.host
#      value: >-
#        10.1.10.212/ky3h
  # 安全设置
  securityContext: {}
    # capabilities:
    #   drop:
    #   - ALL
    # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000
  # 资源配置
  resources: {}
    # We usually recommend not to specify default resources and to leave this as a conscious
    # choice for the user. This also increases chances charts run on environments with little
    # resources, such as Minikube. If you do want to specify resources, uncomment the following
    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
    # limits:
    #   cpu: 100m
    #   memory: 128Mi
    # requests:
  #   cpu: 100m
  #   memory: 128Mi
  #镜像挂载
  volumeMounts: {}
    #主机模式
#    - mountPath: /usr/local/skywalking/agent
#      mountPropagation: None
#      name: skywalking-agent
    #PVC模式
#    - name: atm-data
#        persistentVolumeClaim:
#          claimName: kube-yuncang

如上,可以通过repository和tag指定了镜像下载地址和版本,通过env指定容器运行的环境变量,通过resources可以设定资源限制等。

注意:环境变量如果是整形,要加单引号,如下:

#应用镜像配置
image:
  #仓库地址
  repository: 10.1.10.212:8082/egrand-cloud/egrand-cloud-demo-server
  #镜像版本
  tag: "prod-latest"
  env:
    - name: CONSUL_HOST
      value: 10.1.10.179
    - name: CONSUL_PORT
      value: '8500'
5.5.2 Consul Client SideCar配置说明

该容器默认是关闭的,如果有需要可以开启,该容器采用SideCar模式运行在微服务应用容器的旁边,使微服务应用容器中的Consul能够使用127.0.0.1:8500方式实现与Consul Server的通信,而启用该容器需要设置连接Consul Server的配置信息,配置中采用ConfigMap实现,配置如下:

consulAgentConfigMap:
  # 覆盖名称(与config是冲突的,如果填写该值,则在K8S namespace查找配置字典)
  nameOverride: "consul-agent"
  # 配置字典配置信息
  config:
    agent.json: |
      {
        "bind_addr": "0.0.0.0",
        "client_addr": "0.0.0.0",
        "datacenter": "egdPlatform",
        "primary_datacenter": "egdPlatform",
            "encrypt": "EXz7LFN8hpQ4id8EDYiFoQ==",
        "data_dir": "/consul/data",
        "acl": {
            "enabled": true,
            "default_policy": "deny",
            "enable_token_persistence": true,
            "tokens": {
                "agent": "b07df4e4-1183-5496-35fe-0ac6eb726d05"
            }
        },
        "log_level": "INFO",
        "log_file": "/consul/consul.log",
        "log_rotate_duration": "24h",
        "enable_syslog": false,
        "enable_debug": true,
        "connect":{
            "enabled": true
          },
            "ui": true,
        "start_join":[
            "consul-0.consul.kube-platform.svc.cluster.local",
            "consul-1.consul.kube-platform.svc.cluster.local",
            "consul-2.consul.kube-platform.svc.cluster.local"
          ]
      }

如上,为Consul Client连接Consul Server的配置信息,可以采用两种配置方式:

  1. 如果在我们部署的K8S NameSpace下已经新建了一个ConfigMap,可以通过指定参数nameOverride值即可。

    consulAgentConfigMap:
      # 覆盖名称(与config是冲突的,如果填写该值,则在K8S namespace查找配置字典)
      nameOverride: "consul-agent"
    
  2. 如果想新建一个ConfigMap,通过指定config参数值即可

    consulAgentConfigMap:
      # 配置字典配置信息
      config:
        agent.json: |
          {
            "bind_addr": "0.0.0.0",
            "client_addr": "0.0.0.0",
            "datacenter": "egdPlatform",
            "primary_datacenter": "egdPlatform",
                "encrypt": "EXz7LFN8hpQ4id8EDYiFoQ==",
            "data_dir": "/consul/data",
            "acl": {
                "enabled": true,
                "default_policy": "deny",
                "enable_token_persistence": true,
                "tokens": {
                    "agent": "b07df4e4-1183-5496-35fe-0ac6eb726d05"
                }
            },
            "log_level": "INFO",
            "log_file": "/consul/consul.log",
            "log_rotate_duration": "24h",
            "enable_syslog": false,
            "enable_debug": true,
            "connect":{
                "enabled": true
              },
                "ui": true,
            "start_join":[
                "consul-0.consul.kube-platform.svc.cluster.local",
                "consul-1.consul.kube-platform.svc.cluster.local",
                "consul-2.consul.kube-platform.svc.cluster.local"
              ]
          }
    
5.5.3 Skywalking initContainer配置说明

微服务应用部署支持Skywalking配置,这要求微服务应用在启动命令java -cp 中要制定-javaagent=[skywalking包路径]方式才能使用,默认是关闭的,如下:

# 初始化容器配置
initContainer:
  #是否开启
  enabled: false
  #镜像名称
  name: "skywalking-agent-sidecar"
  #仓库地址
  repository: 10.1.10.212:8082/egrand-cloud-deploy/skywalking-agent-sidecar
  #镜像版本
  tag: "latest"
  #镜像拉去策略:Always(总是拉取 pull)、
  #            IfNotPresent(默认值,本地有则使用本地镜像,不拉取)、
  #            Never(只使用本地镜像,从不拉取)
  pullPolicy: IfNotPresent
  # 资源配置
  securityContext: {}
    # capabilities:
    #   drop:
    #   - ALL
    # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000
  # 资源配置
  resources: {}
    # We usually recommend not to specify default resources and to leave this as a conscious
    # choice for the user. This also increases chances charts run on environments with little
    # resources, such as Minikube. If you do want to specify resources, uncomment the following
    # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
    # limits:
    #   cpu: 100m
    #   memory: 128Mi
    # requests:
  #   cpu: 100m
  #   memory: 128Mi
  # 运行参数配置
  args:
    - '-c'
    - >-
      mkdir -p /skywalking/agent && cp -r /usr/skywalking/agent/* /skywalking/agent
  # 运行命令配置
  command:
    - sh

该容器的目的就是将Skywalking需要应用到的第三方包和配置拷比到容器组中,使得微服务应用容器能够访问到,在启用了skywalking之后,还要进行Skywalking服务器连接配置,指定服务器地址和参数,这样采用将微服务应中的链路日志推送到服务器上。这里采用ConfigMap挂接方式进行:

# skywalking配置字典
skywalkingAgentConfigMap:
  # 覆盖名称(与key和value是冲突的,如果填写该值,则在K8S namespace查找配置字典)
  nameOverride: "skywalking-config"
  # 配置字典配置信息
  config:
    agent.config: |
      agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
      collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:10.1.10.178:11800}
      logging.file_name=${SW_LOGGING_FILE_NAME:skywalking-api.log}
      logging.level=${SW_LOGGING_LEVEL:INFO}
      plugin.mount=${SW_MOUNT_FOLDERS:plugins,activations}

如上,具有两种方式:

  1. 如果微服务应用运行的K8S Namespace中已经存在一个ConfigMap,则只需要指定其名称即可:

    # skywalking配置字典
    skywalkingAgentConfigMap:
      # 覆盖名称(与key和value是冲突的,如果填写该值,则在K8S namespace查找配置字典)
      nameOverride: "skywalking-config"
    
  2. 如果想新建一个ConfigMap,则配置config参数即可;

    # skywalking配置字典
    skywalkingAgentConfigMap:
      # 配置字典配置信息
      config:
        agent.config: |
          agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
          collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:10.1.10.178:11800}
          logging.file_name=${SW_LOGGING_FILE_NAME:skywalking-api.log}
          logging.level=${SW_LOGGING_LEVEL:INFO}
          plugin.mount=${SW_MOUNT_FOLDERS:plugins,activations}
    
5.5.3 imagePullSecrets配置说明

Chart中通过定义如下参数来默认给出了镜像拉取认证:

#镜像拉取密文配置
imageCredentials:
  # 覆盖名称(与config是冲突的,如果填写该值,则在K8S namespace查找配置字典)
#  nameOverride: "harborsecret"
  config:
    # 镜像仓库地址
    registry: http://10.1.10.212:8082/
    # 镜像登录名
    username: admin
    # 镜像登录密码
    password: wiki2012

如上,如果想复用命名空间已经存在的Secret,则修改本地的values.yaml如下:

#镜像拉取密文配置
imageCredentials:
  # 覆盖名称(与config是冲突的,如果填写该值,则在K8S namespace查找配置字典)
  nameOverride: "harborsecret"
5.5.4 Service服务配置说明

服务配置参数默认是采用ClusterIP方式暴露端口的,如果没有端口暴露,则无需配置,如果需要配置,可以通过以下方式复写默认配置:

# 服务配置
service:
  # service的类型访问方式。一般为NodePort、ClusterIP、LoadBalancer
  type: NodePort
  # 端口配置
  ports:
    - name: names
      port: 80
      protocol: TCP
      targetPort: 80
      NodePort: 80

6. Jenkins&GitLab配置

本章节介绍Jenkins的公共库配置,如何新建任务并实现与GitLab的结合。

1. 利用Jenkins公共库配置,规范发布流程;
2. 利用Jenkins的WebHook功能实现与GitLab通信;

6.1 Jenkins Library公共库配置

配置Jenkins Library目的是能够在Jenkinsfile文件中通过"@Library"方式远程加载出来,所以需要指定其名称以及位置等信息。登录到Jenkins管理界面,依次点击”系统设置->系统配置“ 找到"Global Pipeline Libraries",然后如图配置即可:


6.2 Jenkins单模块任务配置

需要发布一个应用时,按如下步骤操作(以下以egrand-cloud-demo-server发布为例)



如上,就是一个单模块的微服务应用。

6.2.1 新建Jenkins任务
  1. 新建Jenkins任务
    点击【新建任务】按钮,进入如下页面:



    输入一个任务名称,选择”流水线“构建。
    注:任务名称必须以英文开头,且长度不能超过30个字符;(该限制是在发布K8S POD时要求的限制)

  2. 点击【确认】按钮,如下图:


  3. 配置”构建触发器“,点击页签”构建触发器“,如下图:



    选择”Generic Webhook Trigger",并在“Post content parameters”中添加如下参数:

variable Expression Description
branch $.ref JSONPath 获取WebHook中GitLab的分支信息参数
gitUrl $.project.git_http_url JSONPath 获取WebHook中GitLab的http地址信息参数
gitCommitId $.after JSONPath 获取WebHook中GitLab最新的提交id号
gitPreCommitId $.before JSONPath 获取WebHook中GitLab上一次提交id号

然后,查找到以下界面:



设置GitLab需要传递的Token值,这个值会在GitLab发起WebHook的时候传递过来,用于关联GitLab项目和具体任务。

  1. 配置流水线,点击页签【流水线】,如下:



    如上图,指定了Jenkins Library在GitLab上的地址,同时设置了Jenkinsfile的脚本路径。

  2. 点击【保存】即可。
6.2.2 GitLab WebHook配置

上一步我们已经完成了一个任务的创建,并且在第3步中得到了一个Token值,现在需要在GitLab上将项目和Jenkins任务关联起来,需要登录GitLab进行操作:

  1. 登录GitLab管理界面,查找到需要关联的GitLab项目


  2. 在该项目下选择菜单“Settings-->Integrations",进入Hook界面配置:



    如上图,输入URL地址,规范为:http://[Jenkins访问地址]:[Jenkins访问端口]/generic-webhook-trigger/invoke?token=[在Jenkins任务中的设置的token值]

  3. 点击按钮[Save]即可。


  4. 可以通过[Test]下拉菜单来进行测试,测试是否能够正常触发Jenkins任务。

6.3 Jenkins多模块任务配置

需要发布一个应用时,按如下步骤操作(以下以egrand-cloud-project发布为例)


如上图,为多模块的应用,现在需要发布egrand-cloud-ram-server模块。

6.3.1 新建Jenkins任务
  1. 新建Jenkins任务
    点击【新建任务】按钮,进入如下页面:



    输入一个任务名称,选择”流水线“构建。
    注:任务名称必须以英文开头,且长度不能超过30个字符;(该限制是在发布K8S POD时要求的限制)

  2. 点击【确认】按钮,如下图:


  3. 配置”构建触发器“,点击页签”构建触发器“,如下图:


选择”Generic Webhook Trigger",并在“Post content parameters”中添加如下参数:

variable Expression Description
branch $.ref JSONPath 获取WebHook中GitLab的分支信息参数
gitUrl $.project.git_http_url JSONPath 获取WebHook中GitLab的http地址信息参数
gitCommitId $.after JSONPath 获取WebHook中GitLab最新的提交id号
gitPreCommitId $.before JSONPath 获取WebHook中GitLab上一次提交id号
commits $.commits[0].['modified','added','removed'][*] JSONPath 获取WebHook中GitLab提交的最新日志记录,用于判断是否为本模块。

配置GitLab在发起WebHook时的Token,配置以下界面:



设置GitLab需要传递的Token值,这个值会在GitLab发起WebHook的时候传递过来,用于关联GitLab项目和具体任务。



设置 Optional filter,用于对提交的最新日志做正则表达式运算,判断是否提交的代码包含该任务。
  1. 配置流水线,点击页签【流水线】,如下:



    设置Jenkinsfile文件的GitLab地址和路径,这里引用Jenkins Library公共的Jenkinsfile文件。

  2. 点击【保存】即可。
6.3.2 GitLab WebHook配置

Jenkins Library存储在GitLab上,具体目录结构如下:

egrand-cloud-jenkins-library
|--- deploy
|      |--- Jenkinsfile     // 公共的Jenkinsfile,项目在创建Jenkins任务时,引用该文件开始构建,该文件调用vars/build.groovy的build方法
|--- vars
       |--- build.groovy    // 构建主文件,为Jenkinsfile提供调用方法build
       |--- gitlab.groovy   // 子流程步骤,供build.groovy调用,完成从GitLab上下载源代码
       |--- jib.groovy      // 子流程步骤,供build.groovy调用,完成maven插件jib构建,构建镜像并推送到Harbor中
       |--- kubecm.groovy   // 子流程步骤,供build.groovy调用,完成kubectl config环境切换和Helm uninstall/install操作,部署到K8S集群
       |--- sonar.groovy    // 子流程步骤,供build.groovy调用,完成静态代码分析,并推送到SonarQube服务器;

7. K8S集群环境

K8S集群环境分为测试环境和生成环境,为了保持发布时的统一处理,规定如下:

1. GitLab master分支 ----> K8S生产环境
2. GitLab dev分支    ----> K8S测试环境

具体切换时使用kubectl config 工具切换。
注:在部署时,会删除掉上次部署的pod和本次部署的pod先,然后再部署;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容