云原生 DevOps 如何做到 Jenkins 开箱即用

前言

随着 Docker 和 K8S 的普及,云原生时代已经到来,开发工程师对应用环境的掌控力进一步加强,运维成本进一步降低。DevOps 采用容器技术更是如虎添翼,持续集成更快更灵活,部署更简单。大多数会选择使用 Jenkins 完成持续集成能力。

场景

在搭建一个云原生 DevOps 平台的时候,不管是 SaaS 化还是私有化的都需要部署前后端项目和基础设施(mysql、rabbitmq...)等。如果该 DevOps 平台要是使用了 Jenkins,则需要部署一套 Jenkins。如果使用一个个的手工部署,部署效率低下,并且出错概率大。如果使用一键部署方式,编写好一键部署脚本,可能只需要做少量的配置,一行命令,一个脚本多处运行。但是如果使用一键部署,Jenkins 初始化时是一个无任何配置的,比如账号密码、 API Token、DevOps 执行 pipeline 需要用到的插件等。难道我们每次部署 Jenkins 都需要人工去操作一遍吗?有没有什么更好的方式能做到 Jenkins 开箱即用?

分析问题

根据以上场景进行分析,搭建云原生 DevOps 平台遇到的主要问题:
• 默认无账号密码和 API Token,无法调用 Jenkins API
• 私有化部署时,如果无外网,Jenkins 插件如何安装
• 使用 kubernetes-plugin 插件,默认无 Cloud 相关配置,无法调用 K8S 创建 slave pod
解决问题
通过定制化 Jenkins 镜像可以做:
• 创建好的账号密码和 API Token,通过一键部署 DevOps 服务和 Jenkins,即可通过账号密码或 API Token 调用 Jenkins API
• 安装好需要的插件,就算在无外网环境下,也无需下载插件了
• 配置好 K8S 相关配置,可以快速使用 Jenkins kubernetes-plugin 插件

如何制作镜像

Jenkins dockerfile 参考

请参考官方提供 ‣‣‣ Jenkins dockerfile

项目目录如下:

image.png

可以根据具体情况选择不同的 dockerfile,如我选择的是 Dockerfile-alpine ,基于 Dockerfile-alpine 做定制配置。

官方 Dockerfile-alpine 文件内容如下:

FROM openjdk:8-jdk-alpine

RUN apk add --no-cache \
  bash \
  coreutils \
  curl \
  git \
  git-lfs \
  openssh-client \
  tini \
  ttf-dejavu \
  tzdata \
  unzip

ARG user=jenkins
ARG group=jenkins
ARG uid=1000
ARG gid=1000
ARG http_port=8080
ARG agent_port=50000
ARG JENKINS_HOME=/var/jenkins_home
ARG REF=/usr/share/jenkins/ref

ENV JENKINS_HOME $JENKINS_HOME
ENV JENKINS_SLAVE_AGENT_PORT ${agent_port}
ENV REF $REF

# Jenkins is run with user `jenkins`, uid = 1000
# If you bind mount a volume from the host or a data container,
# ensure you use the same uid
RUN mkdir -p $JENKINS_HOME \
  && chown ${uid}:${gid} $JENKINS_HOME \
  && addgroup -g ${gid} ${group} \
  && adduser -h "$JENKINS_HOME" -u ${uid} -G ${group} -s /bin/bash -D ${user}

# Jenkins home directory is a volume, so configuration and build history
# can be persisted and survive image upgrades
VOLUME $JENKINS_HOME

# $REF (defaults to `/usr/share/jenkins/ref/`) contains all reference configuration we want
# to set on a fresh new installation. Use it to bundle additional plugins
# or config file with your custom jenkins Docker image.
RUN mkdir -p ${REF}/init.groovy.d

# jenkins version being bundled in this docker image
ARG JENKINS_VERSION
ENV JENKINS_VERSION ${JENKINS_VERSION:-2.60.3}

# jenkins.war checksum, download will be validated using it
ARG JENKINS_SHA=2d71b8f87c8417f9303a73d52901a59678ee6c0eefcf7325efed6035ff39372a

# Can be used to customize where jenkins.war get downloaded from
ARG JENKINS_URL=https://repo.jenkins-ci.org/public/org/jenkins-ci/main/jenkins-war/${JENKINS_VERSION}/jenkins-war-${JENKINS_VERSION}.war

# could use ADD but this one does not check Last-Modified header neither does it allow to control checksum
# see https://github.com/docker/docker/issues/8331
RUN curl -fsSL ${JENKINS_URL} -o /usr/share/jenkins/jenkins.war \
  && echo "${JENKINS_SHA}  /usr/share/jenkins/jenkins.war" | sha256sum -c -

ENV JENKINS_UC https://updates.jenkins.io
ENV JENKINS_UC_EXPERIMENTAL=https://updates.jenkins.io/experimental
ENV JENKINS_INCREMENTALS_REPO_MIRROR=https://repo.jenkins-ci.org/incrementals
RUN chown -R ${user} "$JENKINS_HOME" "$REF"

# for main web interface:
EXPOSE ${http_port}

# will be used by attached slave agents:
EXPOSE ${agent_port}

ENV COPY_REFERENCE_FILE_LOG $JENKINS_HOME/copy_reference_file.log

USER ${user}

COPY jenkins-support /usr/local/bin/jenkins-support
COPY jenkins.sh /usr/local/bin/jenkins.sh
COPY tini-shim.sh /bin/tini
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/jenkins.sh"]

# from a derived Dockerfile, can use `RUN plugins.sh active.txt` to setup $REF/plugins from a support bundle
COPY plugins.sh /usr/local/bin/plugins.sh
COPY install-plugins.sh /usr/local/bin/install-plugins.sh

从文件内容中可分析出

  • ENV JENKINS_VERSION ${JENKINS_VERSION:-2.60.3} 指定了 Jenkins 版本
  • ARG JENKINS_SHA=2d71b8f87c8417f9303a73d52901a59678ee6c0eefcf7325efed6035ff39372a 配置了 jenkins.war 的 sha256 值,用于校验下载的 Jenkins.war sha256 和配置用于校验的 sha256 是否一致

定制步骤

  • 运行 docker run -p 8080:8080 -d -v /root/jenkins_home:/var/jenkins_home jenkins/jenkins:${JENKINS_VERSION} ,将 Jenkins 通过 docker 运行起来,并且把 jenkins_home 目录挂载出来,便于我们获取到 Jenkins 数据,后面的步骤中还需使用这些数据
  • 通过 Jenkins Web 端进行操作,配置好账号密码,安装所需插件,配置所需信息等
  • 选择需要的 Jenkins 版本,通过修改 ENV JENKINS_VERSION ${JENKINS_VERSION:-2.60.3} 配置中的版本号
  • 通过 wget https://repo.jenkins-ci.org/public/org/jenkins-ci/main/jenkins-war/${JENKINS_VERSION}/jenkins-war-${JENKINS_VERSION}.war 获取到 jenkins.war,执行 sha256sum jenkins-war-${JENKINS_VERSION}.war 获取 jenkins.war sha256 值,并且填写到 ARG JENKINS_SHA 配置中
  • COPY jenkins-support /usr/local/bin/jenkins-support 前面添加 COPY ${PATH}/jenkins_home /var/jenkins_home ,将定制数据打入镜像中

如果不想每次都去远程下载 jenkins.war,可以先手动下载,再 COPY 到镜像中

如需指定时区,配置以下内容即可

RUN ln -sf /usr/share/zoneinfo/Asia/ShangHai /etc/localtime

RUN echo "Asia/Shanghai" > /etc/timezone

这样我们就完成了第一步,将需要的数据已经初始化到镜像中,只要通过这个 Jenkins 镜像部署的,都可以达到开箱即用的目的,那么我们继续说一下如何部署?不同的部署方式需要注意什么事项?

如何部署

Docker

使用 docker 或者 docker-compose 部署 Jenkins,如果需要数据持久化,只需要通过 volume 将 /var/jenkins_home 目录挂载到主机即可。

eg:

docker run -d -p 8080:8080 自定义镜像地址

K8S

通过 K8S 部署 Jenkins,可持久化也可不持久化,按实际情况决定。

非持久化

eg:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  annotations:
    app: jenkins
  labels:
    app: jenkins
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      serviceAccountName: ${serviceAccount}
      containers:
        - name: jenkins
          image: ${自定义镜像地址}
          imagePullPolicy: Always
          securityContext:
            runAsUser: 0
            privileged: true
          ports:
            - name: web
              containerPort: 8080
            - name: agent
              containerPort: 50000
          env:
            - name: "JAVA_OPTS"
              value: "
                   -Dhudson.model.LoadStatistics.clock=2000
                   -Dhudson.slaves.NodeProvisioner.recurrencePeriod=5000
                   -Dhudson.slaves.NodeProvisioner.initialDelay=0
                   -Dhudson.model.LoadStatistics.decay=0.5
                   -Dhudson.slaves.NodeProvisioner.MARGIN=50
                   -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85
                   -Duser.timezone=Asia/Shanghai
                   "

持久化

eg:

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: jenkins
  annotations:
    app: jenkins
  labels:
    app: jenkins
spec:
  replicas: 1
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
    spec:
      securityContext:
        runAsUser: 0
      serviceAccountName: ${serviceAccount}
      initContainers:
        - name: backup-data
          image: {自定义镜像地址}
          imagePullPolicy: Always
          command: [ "cp", "-r", "/var/jenkins_home", "/var/jenkins_home_bak"]
          volumeMounts:
            - mountPath: /var/jenkins_home_bak
              name: jenkins-home-bak
        - name: revert-data
          image: {自定义镜像地址}
          imagePullPolicy: Always
          command: [ "cp", "-r", "/var/jenkins_home_bak/jenkins_home", "/var"]
          volumeMounts:
            - mountPath: /var/jenkins_home_bak
              name: jenkins-home-bak
            - mountPath: /var/jenkins_home
              name: jenkins-home
        - name: copy-default-config
          image: {自定义镜像地址}
          imagePullPolicy: Always
          command: [ "sh", "/var/jenkins_config/apply_config.sh" ]
          volumeMounts:
            - mountPath: /var/jenkins_home
              name: jenkins-home
            - mountPath: /var/jenkins_config
              name: jenkins-config
            - mountPath: /usr/share/jenkins/ref/plugins/
              name: plugin-dir
            - mountPath: /usr/share/jenkins/ref/secrets/
              name: secrets-dir
      containers:
        - name: jenkins
          image: {自定义镜像地址}
          imagePullPolicy: Always
          env:
            - name: JAVA_OPTS
              value: "-Dhudson.model.LoadStatistics.clock=2000 -Dhudson.slaves.NodeProvisioner.recurrencePeriod=5000 -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.model.LoadStatistics.decay=0.5 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85 -Duser.timezone=Asia/Shanghai
"
          ports:
            - containerPort: 8080
              name: http
            - containerPort: 50000
              name: slavelistener
          livenessProbe:
            httpGet:
              path: /login
              port: http
            initialDelaySeconds: 5
          readinessProbe:
            httpGet:
              path: /login
              port: http
            initialDelaySeconds: 5
          volumeMounts:
            - mountPath: /var/jenkins_home
              name: jenkins-home
              readOnly: false
            - mountPath: /var/jenkins_config
              name: jenkins-config
              readOnly: true
            - mountPath: /usr/share/jenkins/ref/plugins/
              name: plugin-dir
              readOnly: false
            - mountPath: /usr/share/jenkins/ref/secrets/
              name: secrets-dir
              readOnly: false
      volumes:
      - name: jenkins-config
        configMap:
          name: jenkins-config
      - name: plugin-dir
        emptyDir: {}
      - name: secrets-dir
        emptyDir: {}
      - name: jenkins-home-bak
        emptyDir: {}
      - name: jenkins-home
        persistentVolumeClaim:
          claimName: jenkins // 使用 PVC 进行持久化
         
 ---
 # config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: jenkins-config
data:
  config.xml: |-
    <?xml version='1.0' encoding='UTF-8'?>
    <hudson>
      <disabledAdministrativeMonitors/>
      <version>2.222.3-alpine</version>
      <installStateName>RUNNING</installStateName>
      <numExecutors>10</numExecutors>
      <mode>NORMAL</mode>
      <useSecurity>true</useSecurity>
      <authorizationStrategy class="hudson.security.FullControlOnceLoggedInAuthorizationStrategy">
        <denyAnonymousReadAccess>true</denyAnonymousReadAccess>
      </authorizationStrategy>
      <securityRealm class="hudson.security.HudsonPrivateSecurityRealm">
        <disableSignup>true</disableSignup>
        <enableCaptcha>false</enableCaptcha>
      </securityRealm>
      <disableRememberMe>false</disableRememberMe>
      <projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
      <workspaceDir>${JENKINS_HOME}/workspace/${ITEM_FULLNAME}</workspaceDir>
      <buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
      <markupFormatter class="hudson.markup.EscapedMarkupFormatter"/>
      <jdks/>
      <viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
      <myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
      <clouds>
        <org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud plugin="kubernetes@">
          <name>kubernetes</name>
          <templates>
            <org.csanchez.jenkins.plugins.kubernetes.PodTemplate>
              <inheritFrom></inheritFrom>
              <name>default</name>
              <instanceCap>2147483647</instanceCap>
              <idleMinutes>0</idleMinutes>
              <label>jenkins-jenkins-slave</label>
              <nodeSelector></nodeSelector>
                <nodeUsageMode>NORMAL</nodeUsageMode>
              <volumes>
              </volumes>
              <containers>
                <org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
                  <name>jnlp</name>
                  <image>jenkins/jnlp-slave:4.0.1-1</image>
                  <privileged>false</privileged>
                  <alwaysPullImage>false</alwaysPullImage>
                  <workingDir>/home/jenkins</workingDir>
                  <command></command>
                  <args>${computer.jnlpmac} ${computer.name}</args>
                  <ttyEnabled>false</ttyEnabled>
                  <resourceRequestCpu>200m</resourceRequestCpu>
                  <resourceRequestMemory>256Mi</resourceRequestMemory>
                  <resourceLimitCpu>200m</resourceLimitCpu>
                  <resourceLimitMemory>256Mi</resourceLimitMemory>
                  <envVars>
                    <org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                      <key>JENKINS_URL</key>
                      <value>http://jenkins:8080</value>
                    </org.csanchez.jenkins.plugins.kubernetes.ContainerEnvVar>
                  </envVars>
                </org.csanchez.jenkins.plugins.kubernetes.ContainerTemplate>
              </containers>
              <envVars/>
              <annotations/>
              <imagePullSecrets/>
              <nodeProperties/>
            </org.csanchez.jenkins.plugins.kubernetes.PodTemplate></templates>
          <serverUrl>https://kubernetes.default</serverUrl>
          <skipTlsVerify>false</skipTlsVerify>
          <namespace>default</namespace>
          <jenkinsUrl>http://jenkins:8080</jenkinsUrl>
          <jenkinsTunnel>jenkins-agent:50000</jenkinsTunnel>
          <containerCap>10</containerCap>
          <retentionTimeout>5</retentionTimeout>
          <connectTimeout>0</connectTimeout>
          <readTimeout>0</readTimeout>
        </org.csanchez.jenkins.plugins.kubernetes.KubernetesCloud>
      </clouds>
      <quietPeriod>5</quietPeriod>
      <scmCheckoutRetryCount>0</scmCheckoutRetryCount>
      <views>
        <hudson.model.AllView>
          <owner class="hudson" reference="../../.."/>
          <name>All</name>
          <filterExecutors>false</filterExecutors>
          <filterQueue>false</filterQueue>
          <properties class="hudson.model.View$PropertyList"/>
        </hudson.model.AllView>
      </views>
      <primaryView>All</primaryView>
      <slaveAgentPort>50000</slaveAgentPort>
      <label></label>
      <nodeProperties/>
      <globalNodeProperties/>
      <noUsageStatistics>true</noUsageStatistics>
    </hudson>
  apply_config.sh: |-
    mkdir -p /usr/share/jenkins/ref/secrets/;
    echo "false" > /usr/share/jenkins/ref/secrets/slave-to-master-security-kill-switch;
    cp -f /var/jenkins_config/config.xml /var/jenkins_home;
  plugins.txt: |-

可通过 PVC 进行持久化,也可通过 nodeSelector 选择一台主机通过 volume 进行挂载

Helm

请参考官方提供 ‣‣‣ jenkins-helm-charts

由于官方提供的 helm charts 不会保留自定义镜像中的数据,需要我们通过改造官方脚本进行自定义,在templates/jenkins-master-deployment.yamlinitContainers 中,将自定义的 config.xml 覆盖容器中的,并且执行 config.yaml 中定义的脚本。这样只能做到,自定义 config.xml 文件和在线安装插件等操作,由于挂载了 /var/jenkins_home 目录,且挂载的是一个空目录,这样会导致原镜像中的数据也置空,基于这个目的,我们进行 templates/jenkins-master-deployment.yaml 的改造。

templates/jenkins-master-deployment.yaml 文件需要新增内容如下:

...
      initContainers:
        - name: "backup-data"
          image: "{{ .Values.Master.Image }}:{{ .Values.Master.ImageTag }}"
          imagePullPolicy: "{{ .Values.Master.ImagePullPolicy }}"
          command: [ "cp", "-r", "/var/jenkins_home", "/var/jenkins_home_bak"]
          volumeMounts:
            - mountPath: /var/jenkins_home_bak
              name: jenkins-home-bak
        - name: "revert-data"
          image: "{{ .Values.Master.Image }}:{{ .Values.Master.ImageTag }}"
          imagePullPolicy: "{{ .Values.Master.ImagePullPolicy }}"
          command: [ "cp", "-r", "/var/jenkins_home_bak/jenkins_home", "/var"]
          volumeMounts:
            - mountPath: /var/jenkins_home_bak
              name: jenkins-home-bak
            - mountPath: /var/jenkins_home
              name: jenkins-home
        - name: "copy-default-config"
          image: "{{ .Values.Master.Image }}:{{ .Values.Master.ImageTag }}"
          imagePullPolicy: "{{ .Values.Master.ImagePullPolicy }}"
          command: [ "sh", "/var/jenkins_config/apply_config.sh" ]
          volumeMounts:
            - mountPath: /var/jenkins_home
              name: jenkins-home
            - mountPath: /var/jenkins_config
              name: jenkins-config
            {{- if .Values.Master.CredentialsXmlSecret }}
            - mountPath: /var/jenkins_credentials
              name: jenkins-credentials
              readOnly: true
            {{- end }}
            {{- if .Values.Master.SecretsFilesSecret }}
            - mountPath: /var/jenkins_secrets
              name: jenkins-secrets
              readOnly: true
            {{- end }}
            {{- if .Values.Master.Jobs }}
            - mountPath: /var/jenkins_jobs
              name: jenkins-jobs
              readOnly: true
            {{- end }}
            - mountPath: /usr/share/jenkins/ref/plugins/
              name: plugin-dir
            - mountPath: /usr/share/jenkins/ref/secrets/
              name: secrets-dir
 ...

新增两个 initContainer,backup-data 用于将原镜像需要备份出来,revert-data 再将数据还原,再通过 copy-default-config 自定义 config.xml 和执行脚本

注意事项

Jenkins config.xml 中使用的 securityRealmhudson.security.LegacySecurityRealm 会导致使用 Jenkins API 请求报错,如果需要通过 Jenkins API 操作,需修改为如下:

<securityRealm class="hudson.security.HudsonPrivateSecurityRealm">
  <disableSignup>true</disableSignup>
  <enableCaptcha>false</enableCaptcha>
</securityRealm>

但是修改成上面这种方式,会导致无法通过 values.yaml 自定义 Jenkins 账号密码,只能使用原镜像初始化的账号密码。

疑问

为什么不适用 docker commit 构建自定义镜像

对于 docker commit 命令,几乎所有的操作都围绕着可读可写层(Read-Write Layer),一次 commit 将可读可写层打包为一个全新的镜像,同时也保证镜像之间的独立性。当然,由于一个镜像同时包含镜像层文件系统内容和镜像 json 文件,因此对于一个 commit 操作,Docker Daemon 还会为镜像产生一个全新的 json 文件。

对于 Docker 容器而言,文件系统视角包含的内容有 Docker 镜像构成的内容(一个可读可写层加上多个只读层)、数据卷 VOLUME 挂载的目录内容,还有类似于 hosts、hostsname 和 resolv.conf 等挂载文件,当然还有一些如 /proc 和 /sys 等虚拟文件系统的内容。commit 操作只专注于可读可写层(Read-Write Layer),因此其他的内容都将不会出现在打包后的镜像中。举例说明,类似于 Jenkins 的数据容器,由于其自身的数据一般都持久化到数据卷 VOLUME 中,因此 Jenkins 在运行过程中产生的数据将不会在 commit 操作后被打包进入镜像。

总结

我们通过定制化 Jenkins 镜像,将业务平台需要的数据初始化进镜像中,能帮我们达到开箱即用的目的,无需人工介入,即降低人工成本,也减少人工出错的概率。

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

推荐阅读更多精彩内容