为了在Kubernetes上搭建RabbitMQ3.7.X Cluster,踩爆无数坑,官方整合了第三方开源项目但没有完整demo,网上的post都是RabbitMQ 3.6.X旧版的部署方案,几经周折,最终弄明白在Kubernetes集群下,基于Kubernetes Discovery,使用hostname方式部署RabbitMQ3.7.X Cluster,总结如下:

1. IP模式

rabbitmq-peer-discovery-k8s是RabbitMQ官方基于第三方开源项目rabbitmq-autocluster开发,对3.7.X版本提供的Kubernetes下的同行发现插件,但官方只提供了一个基于IP模式的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
kind: Service
apiVersion: v1
metadata:
namespace: test-rabbitmq
name: rabbitmq
labels:
app: rabbitmq
type: LoadBalancer
spec:
type: NodePort
ports:
- name: http
protocol: TCP
port: 15672
targetPort: 15672
nodePort: 31672
- name: amqp
protocol: TCP
port: 5672
targetPort: 5672
nodePort: 30672
selector:
app: rabbitmq
---
apiVersion: v1
kind: ConfigMap
metadata:
name: rabbitmq-config
namespace: test-rabbitmq
data:
enabled_plugins: |
[rabbitmq_management,rabbitmq_peer_discovery_k8s].
rabbitmq.conf: |
## Cluster formation. See http://www.rabbitmq.com/cluster-formation.html to learn more.
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s
cluster_formation.k8s.host = kubernetes.default.svc.cluster.local
## Should RabbitMQ node name be computed from the pod's hostname or IP address?
## IP addresses are not stable, so using [stable] hostnames is recommended when possible.
## Set to "hostname" to use pod hostnames.
## When this value is changed, so should the variable used to set the RABBITMQ_NODENAME
## environment variable.
cluster_formation.k8s.address_type = ip
## How often should node cleanup checks run?
cluster_formation.node_cleanup.interval = 30
## Set to false if automatic removal of unknown/absent nodes
## is desired. This can be dangerous, see
## * http://www.rabbitmq.com/cluster-formation.html#node-health-checks-and-cleanup
## * https://groups.google.com/forum/#!msg/rabbitmq-users/wuOfzEywHXo/k8z_HWIkBgAJ
cluster_formation.node_cleanup.only_log_warning = true
cluster_partition_handling = autoheal
## See http://www.rabbitmq.com/ha.html#master-migration-data-locality
queue_master_locator=min-masters
## See http://www.rabbitmq.com/access-control.html#loopback-users
loopback_users.guest = false
---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
name: rabbitmq
namespace: test-rabbitmq
spec:
serviceName: rabbitmq
replicas: 3
template:
metadata:
labels:
app: rabbitmq
spec:
serviceAccountName: rabbitmq
terminationGracePeriodSeconds: 10
containers:
- name: rabbitmq-k8s
image: rabbitmq:3.7
volumeMounts:
- name: config-volume
mountPath: /etc/rabbitmq
ports:
- name: http
protocol: TCP
containerPort: 15672
- name: amqp
protocol: TCP
containerPort: 5672
livenessProbe:
exec:
command: ["rabbitmqctl", "status"]
initialDelaySeconds: 60
periodSeconds: 60
timeoutSeconds: 10
readinessProbe:
exec:
command: ["rabbitmqctl", "status"]
initialDelaySeconds: 20
periodSeconds: 60
timeoutSeconds: 10
imagePullPolicy: Always
env:
- name: MY_POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: RABBITMQ_USE_LONGNAME
value: "true"
# See a note on cluster_formation.k8s.address_type in the config file section
- name: RABBITMQ_NODENAME
value: "rabbit@$(MY_POD_IP)"
- name: K8S_SERVICE_NAME
value: "rabbitmq"
- name: RABBITMQ_ERLANG_COOKIE
value: "mycookie"
volumes:
- name: config-volume
configMap:
name: rabbitmq-config
items:
- key: rabbitmq.conf
path: rabbitmq.conf
- key: enabled_plugins
path: enabled_plugins

在ConfigMap配置项中,指明 cluster_formation.k8s.address_type = ip,也就是说RabbitMQ Node的命名和访问地址是以IP地址作为区分,如rabbit@172.0.5.1

但这样的配置会产生比较大的问题,如果我们使用pv和pvc去做数据的持久化,那么每个节点的配置和数据存储都会放在rabbit@172.0.5.1这样的文件夹下,而Kubernetes集群中,Pod的IP都是不稳定的,当有RabbitMQ Node的Pod挂掉后,重新创建的Pod IP可能会变,这就会使得节点的配置和数据全部丢失。

所以我们更希望RabbitMQ Node的命名是以一定规则编写的相对稳定的名称,如rabbit@rabbit-0,这就需要修改 cluster_formation.k8s.address_type = hostname,以启用hostname模式。

但直接修改address_type 并不能满足要求,注释部分也描述了“Set to hostname to use pod hostnames. When this value is changed, so should the variable used to set the RABBITMQ_NODENAME”。那么RABBITMQ_NODENAME该如何设置,就必须先要了解如何用hostname访问pod

2. Pod与Service的DNS

Kubernetes官方讲述了如何用hostname访问service和pod:dns-pod-service

其中对于service,可以直接使用my-svc.my-namespace.svc.cluster.local进行访问;而对于pod,则需使用pod-ip-address.my-namespace.pod.cluster.local进行访问,但这里却仍显式的应用到了pod的ip。我们希望脱离ip对pod进行访问,很不幸的是,pod确实无法直接通过hostname访问,不过却有个曲线救国的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
apiVersion: v1
kind: Service
metadata:
name: default-subdomain # 和pod的subdomain相同
spec:
selector:
name: busybox
clusterIP: None # clusterIP: None表示这是一个headless service
ports:
- name: foo # 没啥用
port: 1234
targetPort: 1234
---
apiVersion: v1
kind: Pod
metadata:
name: busybox1
labels:
name: busybox
spec:
hostname: busybox-1 # 默认使用metadata.name作为hostname,也可指定设置
subdomain: default-subdomain
containers:
- image: busybox
command:
- sleep
- "3600"
name: busybox

如上面代码所示,我们需要一个headless service来作为中介,这样就可以使用busybox-1.default-subdomain.default.svc.cluster.local来访问pod了(hostname.subdomain.my-namespace.svc.cluster.local)

3. Statefulset 与Headless Service

了解了如何用hostname访问Pod还不足以解决问题,在RabbitMQ的配置中,我们使用的是StatefulSet,那么StatefulSet如何用Headless Service去做Pod的hostname访问呢?

Kubernetes(StatefulSets在1.9版本后已经是一个稳定功能)官方也给出了详细的说明:statefulset

Demo和注释如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: v1
kind: Service
metadata:
name: nginx
labels:
app: nginx
spec:
ports:
- port: 80
name: web
clusterIP: None # 是一个headless service
selector:
app: nginx
---
apiVersion: apps/v1 # 需要注意如果是apps/v1,.spec.selector.matchLabels和.spec.template.metadata.labels要相同;如果是apps/v1beta,可以省略.spec.selector.matchLabels
kind: StatefulSet
metadata:
name: web
spec:
selector:
matchLabels:
app: nginx # 需要与 .spec.template.metadata.labels 相同,但无需与headless service name相同
serviceName: "nginx" # 需要与headless service name相同
replicas: 3
template:
metadata:
labels:
app: nginx # 需要与 .spec.selector.matchLabels 相同,但无需与headless service name相同
spec:
terminationGracePeriodSeconds: 10
containers:
- name: nginx
image: k8s.gcr.io/nginx-slim:0.8
ports:
- containerPort: 80
name: web

需要特别注意的是,网上很多例子的StatefulSet用的apps/v1beta

4. hostname模式

在我查找的众多资料中,在Kubernetes中

讲RabbitMQ 3.6.X部署的,https://www.kubernetes.org.cn/2629.html 这篇讲的比较清楚

讲RabbitMQ 3.7.X部署的,https://habr.com/company/eastbanctech/blog/419817 这篇俄文的Post讲的比较清楚,但它也是用的apps/v1beta,同时有大量的重复配置,不知道哪些可用哪些无用,还有一个最致命的问题是按照它的配置部署后,readinessProbe老报错,说DNS解析出现问题。几经折腾,才明白因为用Headless Service去做Pod的hostname访问,需要等Pod和Service都启动后才能访问,而readiness探针还没等DNS正常就去探查服务是否可用,所以才会误认为服务不可达,最终无法启动Pod。解决办法是给Headless Service设置publishNotReadyAddresses: true

我的配置文件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
apiVersion: v1
kind: Namespace
metadata:
name: rabbitmq
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: rabbitmq
namespace: rabbitmq
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: endpoint-reader
namespace: rabbitmq
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: endpoint-reader
namespace: rabbitmq
subjects:
- kind: ServiceAccount
name: rabbitmq
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: endpoint-reader
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: rabbitmq-data
labels:
release: rabbitmq-data
namespace: rabbitmq
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /rabbit
server: xxxxx # nas地址
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: rabbitmq-data-claim
namespace: rabbitmq
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
selector:
matchLabels:
release: rabbitmq-data
---
# headless service 用于使用hostname访问pod
kind: Service
apiVersion: v1
metadata:
name: rabbitmq-headless
namespace: rabbitmq
spec:
clusterIP: None
# publishNotReadyAddresses, when set to true, indicates that DNS implementations must publish the notReadyAddresses of subsets for the Endpoints associated with the Service. The default value is false. The primary use case for setting this field is to use a StatefulSet's Headless Service to propagate SRV records for its Pods without respect to their readiness for purpose of peer discovery. This field will replace the service.alpha.kubernetes.io/tolerate-unready-endpoints when that annotation is deprecated and all clients have been converted to use this field.
# 由于使用DNS访问Pod需Pod和Headless service启动之后才能访问,publishNotReadyAddresses设置成true,防止readinessProbe在服务没启动时找不到DNS
publishNotReadyAddresses: true
ports:
- name: amqp
port: 5672
- name: http
port: 15672
selector:
app: rabbitmq
---
# 用于暴露dashboard到外网
kind: Service
apiVersion: v1
metadata:
namespace: rabbitmq
name: rabbitmq-service
spec:
type: NodePort
ports:
- name: http
protocol: TCP
port: 15672
targetPort: 15672
nodePort: 15672
- name: amqp
protocol: TCP
port: 5672
targetPort: 5672
selector:
app: rabbitmq
---
apiVersion: v1
kind: ConfigMap
metadata:
name: rabbitmq-config
namespace: rabbitmq
data:
enabled_plugins: |
[rabbitmq_management,rabbitmq_peer_discovery_k8s].
rabbitmq.conf: |
cluster_formation.peer_discovery_backend = rabbit_peer_discovery_k8s
cluster_formation.k8s.host = kubernetes.default.svc.cluster.local
cluster_formation.k8s.address_type = hostname
cluster_formation.node_cleanup.interval = 10
cluster_formation.node_cleanup.only_log_warning = true
cluster_partition_handling = autoheal
queue_master_locator=min-masters
loopback_users.guest = false
cluster_formation.randomized_startup_delay_range.min = 0
cluster_formation.randomized_startup_delay_range.max = 2
# 必须设置service_name,否则Pod无法正常启动,这里设置后可以不设置statefulset下env中的K8S_SERVICE_NAME变量
cluster_formation.k8s.service_name = rabbitmq-headless
# 必须设置hostname_suffix,否则节点不能成为集群
cluster_formation.k8s.hostname_suffix = .rabbitmq-headless.rabbitmq.svc.cluster.local
# 内存上限
vm_memory_high_watermark.absolute = 1.6GB
# 硬盘上限
disk_free_limit.absolute = 2GB
---
# 使用apps/v1版本代替apps/v1beta
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: rabbitmq
namespace: rabbitmq
spec:
serviceName: rabbitmq-headless # 必须与headless service的name相同,用于hostname传播访问pod
selector:
matchLabels:
app: rabbitmq # 在apps/v1中,需与 .spec.template.metadata.label 相同,用于hostname传播访问pod,而在apps/v1beta中无需这样做
replicas: 3
template:
metadata:
labels:
app: rabbitmq # 在apps/v1中,需与 .spec.selector.matchLabels 相同
# 设置podAntiAffinity
annotations:
scheduler.alpha.kubernetes.io/affinity: >
{
"podAntiAffinity": {
"requiredDuringSchedulingIgnoredDuringExecution": [{
"labelSelector": {
"matchExpressions": [{
"key": "app",
"operator": "In",
"values": ["rabbitmq"]
}]
},
"topologyKey": "kubernetes.io/hostname"
}]
}
}
spec:
serviceAccountName: rabbitmq
terminationGracePeriodSeconds: 10
containers:
- name: rabbitmq
image: registry-vpc.cn-shenzhen.aliyuncs.com/heygears/rabbitmq:3.7
resources:
limits:
cpu: 0.5
memory: 2Gi
requests:
cpu: 0.3
memory: 2Gi
volumeMounts:
- name: config-volume
mountPath: /etc/rabbitmq
- name: rabbitmq-data
mountPath: /var/lib/rabbitmq/mnesia
ports:
- name: http
protocol: TCP
containerPort: 15672
- name: amqp
protocol: TCP
containerPort: 5672
livenessProbe:
exec:
command: ["rabbitmqctl", "status"]
initialDelaySeconds: 60
periodSeconds: 60
timeoutSeconds: 5
readinessProbe:
exec:
command: ["rabbitmqctl", "status"]
initialDelaySeconds: 20
periodSeconds: 60
timeoutSeconds: 5
imagePullPolicy: Always
env:
- name: HOSTNAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: RABBITMQ_USE_LONGNAME
value: "true"
- name: RABBITMQ_NODENAME
value: "rabbit@$(HOSTNAME).rabbitmq-headless.rabbitmq.svc.cluster.local"
# 若在ConfigMap中设置了service_name,则此处无需再次设置
# - name: K8S_SERVICE_NAME
# value: "rabbitmq-headless"
- name: RABBITMQ_ERLANG_COOKIE
value: "mycookie"
volumes:
- name: config-volume
configMap:
name: rabbitmq-config
items:
- key: rabbitmq.conf
path: rabbitmq.conf
- key: enabled_plugins
path: enabled_plugins
- name: rabbitmq-data
persistentVolumeClaim:
claimName: rabbitmq-data-claim

至此,终于在Kubernetes上部署完成RabbitMQ Cluster 3.7.X

评论和共享

目录

  1. 一、一机多Jenkins Slave
  2. 二、 二次开发Jenkins 钉钉通知插件
  3. 三、 DevOps解决方案

前言:

在上一篇文章中,我们已经在K8S集群部署了Jenkins、Harbor和EFK。作为本系列最后一篇文章,将通过实际案例串联所有的基础软件服务,基于K8S做DevOps。

整体的业务流程如下图所示:

1

一、一机多Jenkins Slave

由于业务需要,我们的自动化测试需要基于windows做web功能测试,每一个测试任务独占一个windows用户桌面,所以我们首先要给Jenkins配置几个Windows的Slave Node.在我之前的post《持续集成CI实施指南三–jenkins集成测试》中详细讲解了给Jenkins添加Node的方法步骤。 本篇无需重复,但这里主要讲的是,如何在一台Windows服务器上搭建多个Jenkins Node,供多用户使用。

  • 在目标机上建立多个用户,如下图所示:

    2

  • 用Administrator用户安装JDK

  • 在Jenkins的节点管理建立三个Node,分别为WinTester01、WinTester02、WinTester03,配置如下

    3

  • 在目标机的Administrator,用IE打开Jenkins并进入节点管理,在WinTester01、WinTester02、WinTester03中分别点击“Launch”启动Slave

    4

  • 确认启动成功后,点击“File”下的“Install as service”

    5

  • 三个Slave都启动后,可以在服务管理器看到

    6

  • 除了Jenkins Slave1无需配置,Slave2和Slave3都需要右键进入属性,修改登录用户分别为JenkinsSlave2和JenkinsSlave3

    7

通过上面的配置,可以在一台目标机部署三个用户对应三个Jenkins Slave以满足我们的业务需求。

二、 二次开发Jenkins 钉钉通知插件

在整个DevOps的业务流程图上,我们想使用钉钉作为通知方式,相比邮件而言,实时性和扩展性都很高。在2018年4月,Jenkins的钉钉通知插件有两款,分别是Dingding JSON PusherDingding notification plugin,前者长期未更新,已经不能使用,后者可以在非Pipeline模式下使用,对于Pipeline则有一些问题。虽然目前,Dingding notification plugin已经更新到1.9版本并支持了Pipeline,但在当时,我们不得不在1.4版本的基础上做二次开发。

整体开发经过参考《Jenkins项目实战之-钉钉提醒插件二次开发举例》,总体来说还是比较简单:

  • 修改”src/main/java/com/ztbsuper/dingtalk/DingTalkNotifier.java”,钉钉的消息API类型有文本、link、markdown、card等,我们这里把通知接口改成文本类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    public class DingTalkNotifier extends Notifier implements SimpleBuildStep {
    private String accessToken;
    private String message;
    private String imageUrl;
    private String messageUrl;
    @DataBoundConstructor
    public DingTalkNotifier(String accessToken, String message, String imageUrl, String messageUrl) {
    this.accessToken = accessToken; //钉钉的accesstoken
    this.message = message; //消息主体
    this.imageUrl = imageUrl; //缩略图
    this.messageUrl = messageUrl; //消息的链接来源,一般是jenkins的build url
    }
    public String getAccessToken() {
    return accessToken;
    }
    public String getMessage() {
    return message;
    }
    public String getImageUrl() {
    return imageUrl;
    }
    public String getMessageUrl() {
    return messageUrl;
    }
    @Override
    public void perform(@Nonnull Run<?, ?> run, @Nonnull FilePath filePath, @Nonnull Launcher launcher, @Nonnull TaskListener taskListener) throws InterruptedException, IOException {
    String buildInfo = run.getFullDisplayName();
    if (!StringUtils.isBlank(message)) {
    sendMessage(LinkMessage.builder()
    .title(buildInfo)
    .picUrl(imageUrl)
    .text(message)
    .messageUrl(messageUrl)
    .build());
    }
    }
    private void sendMessage(DingMessage message) {
    DingTalkClient dingTalkClient = DingTalkClient.getInstance();
    try {
    dingTalkClient.sendMessage(accessToken, message);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    @Override
    public BuildStepMonitor getRequiredMonitorService() {
    return BuildStepMonitor.NONE;
    }
    @Symbol("dingTalk")
    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> {
    @Override
    public boolean isApplicable(Class<? extends AbstractProject> aClass) {
    return true;
    }
    @Nonnull
    @Override
    public String getDisplayName() {
    return Messages.DingTalkNotifier_DescriptorImpl_DisplayName();
    }
    }
    }
  • 用maven打包

    maven需要安装java环境,为了方便,我直接run一个maven的docker image,编译完成后把hpi文件send出来

  • 在jenkins的插件管理页面上传hpi文件

    8

  • 在钉钉群中开启自定义机器人

    9

  • 找到accesstoken

    10

  • 在jenkins pipeline中可以使用以下命令发送信息到钉钉群

    1
    dingTalk accessToken:"2fccafaexxxx",message:"信息",imageUrl:"图片地址",messageUrl:"消息链接"

三、 DevOps解决方案

针对每一个软件项目增加部署目录,目录结构如下:

  • _deploy
    • master
      • deployment.yaml
      • Dockerfile
      • other files
    • test
      • deployment.yaml
      • Dockerfile
      • other files

master和test文件夹用于区分测试环境与生产环境的部署配置

Dockerfile和other files用于生成应用或服务的镜像

如前端vue和nodejs项目的Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 前端项目运行环境的Image,从Harbor获取
FROM xxx/xxx/frontend:1.0.0
RUN mkdir -p /workspace/build && mkdir -p /workspace/run
COPY . /workspace/build
# 编译,生成执行文件,并删除源文件
RUN cd /workspace/build/frontend && \
cnpm install && \
npm run test && \
cp -r /workspace/build/app/* /workspace/run && \
rm -rf /workspace/build && \
cd /workspace/run && \
cnpm install
# 运行项目,用npm run test或run prod区分测试和生产环境
CMD cd /workspace/run && npm run test

又如dotnet core项目的Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# dotnet项目编译环境的Image,从Harbor获取
FROM xxx/xxx/aspnetcore-build:2 AS builder
WORKDIR /app
COPY . .
# 编译
RUN cd /app/xxx
RUN pwd && ls -al && dotnet restore
RUN dotnet publish -c Release -o publish
# dotnet项目运行环境的Image,从Harbor获取
FROM xxx/xxx/aspnetcore:2
WORKDIR /publish
COPY --from=builder /app/xxx/publish .
# 重命名配置文件,中缀test、prod用于区分测试环境和生产环境
RUN mv appsettings.test.json appsettings.json
# 运行
ENTRYPOINT ["dotnet", "xxx.dll"]

deployent.yaml用于执行应用或服务在k8s上的部署

由于deployment有很多配置项可以抽离成公共配置,所以deployment的配置有很多占位变量,占位变量用两个#中间加变量名表示,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
apiVersion: v1
kind: Namespace
metadata:
name: #namespace#
labels:
name: #namespace#
---
apiVersion: v1
data:
.dockerconfigjson: xxxxxxxxxxxxxxxxxxxxxx
kind: Secret
metadata:
name: regcred
namespace: #namespace#
type: kubernetes.io/dockerconfigjson
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: #app#-deploy
namespace: #namespace#
labels:
app: #app#-deploy
spec:
replicas: #replicas#
strategy:
type: Recreate
template:
metadata:
labels:
app: #app#
spec:
containers:
- image: #image#
name: #app#
ports:
- containerPort: #port#
name: #app#
securityContext:
privileged: #privileged#
volumeMounts:
- name: log-volume
mountPath: #log#
- image: #filebeatImage#
name: filebeat
args: [
"-c", "/etc/filebeat.yml"
]
securityContext:
runAsUser: 0
volumeMounts:
- name: config
mountPath: /etc/filebeat.yml
readOnly: true
subPath: filebeat.yml
- name: log-volume
mountPath: /var/log/container/
volumes:
- name: config
configMap:
defaultMode: 0600
name: filebeat-config
- name: log-volume
emptyDir: {}
imagePullSecrets:
- name: regcred
---
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: #namespace#
labels:
app: filebeat
data:
filebeat.yml: |-
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/container/*.log
output.elasticsearch:
hosts: ["#es#"]
tags: ["#namespace#-#app#"]
---
apiVersion: v1
kind: Service
metadata:
name: #app#-service
namespace: #namespace#
labels:
app: #app#-service
spec:
ports:
- port: 80
targetPort: #port#
selector:
app: #app#
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: #app#-ingress
namespace: #namespace#
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: #host#
http:
paths:
- path: #urlPath#
backend:
serviceName: #app#-service
servicePort: 80

其中几个关键变量的解释如下:

  • dockerconfigjson:因为所有的镜像需要从Harbor获取,而Harbor的镜像如果设置为私有权限,就需要提供身份验证,这里的dockerconfigjson就是Harbor的身份信息。生成dockerconfigjson的方法如下:
    • 进入K8S任何一个节点,删除” ~/.docker/config.json “ 文件
    • 使用命令” docker login harbor地址”登录harbor
    • 通过命令” cat ~/.docker/config.json “可以看到harbor的身份验证信息
    • 使用命令” cat /root/.docker/config.json | base64 -w 0 “对信息编码,将生成后的编码填写到deployment.yaml的dockerconfigjson节点即可
  • namespace:同一个项目的不同k8s组件应置于同一个namespace,所以namespace可统一配置,在我们的项目实践中,生产环境的namespace为” 项目名 “,测试环境的namespace为” 项目名-test “
  • app:应用或服务名称
  • image:应用或服务的镜像地址
  • replicas:副本数量
  • port:应用或服务的Pod开放端口
  • log:应用或服务的日志路径,在本系列的第二篇文章中,提到我们的日志方案是给每个应用或服务配一个filebeat,放在同一Pod中,这里只需告知应用或服务的日志的绝对路径,filebeat就能将日志传递到ES中,日志的tag命名方式为” namespace-app”
  • host:在本系列的第一篇文章中,讲了使用nginx ingress做服务暴露与负载。这里的host就是给nginx ingress设置的域名,端口默认都是80,如果需要https,则在外层使用阿里云SLB转发
  • urlPath:很多情况下,如微服务,需要通过相同的域名,不同的一级目录将请求分发到不同的后台,在nginx中,就是location的配置与反向代理,比如host的配置是确定了域名aaa.bbb.com,而urlPath的配置是确定aaa.bbb.com/user/getuser将会被转发到用户服务podIP:podPort/getuser中

以上所有的占位变量都是在Pipeline Script中赋值,关于Jenkins Pipeline的相关内容介绍这里不再多讲,还是去看官方文档靠谱。我们这里将k8s的部署文件deployment.yaml与Jenkinsfile结合,即可做到一个deployment.yaml能适配所有项目,一个Pipeline Script模板能适配所有项目,针对不同的项目,只需在Pipeline Script中给占位变量赋值,大大降低了配置复杂度。下面是一个项目的Jenkins配置示例:

11

对于一个项目,我们只需配置Trigger和Pipeline,上图“Do not allow concurrent builds ”也是通过Pipeline的配置生成的。Pipeline Script示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
pipeline {
// 指定项目在label为jnlp-agent的节点上构建,也就是Jenkins Slave in Pod
agent { label 'jnlp-agent' }
// 对应Do not allow concurrent builds
options {
disableConcurrentBuilds()
}
environment {
// ------ 以下内容,每个项目可能均有不同,按需修改 ------
//author:用于钉钉通知
author="张三"
// branch: 分支,一般是test、 master,对应git从哪个分支拉取代码,也对应究竟执行_deploy文件夹下的test配置还是master配置
branch = "test"
// namespace: myproject-test, myproject,命名空间一般是项目名称,测试环境加test
namespace = "myproject-test"
// hostname:对应deployment中的host
host = "test.aaa.bbb.com"
// appname:对应deployment中的app
app = "myserver"
// port:对应deployment中的port
port= "80"
// replicas:对应deployment中的replicas
replicas = 2
//git repo path:git的地址
git="git@git.aaa.bbb.com/xxx.git"
//log:对应deployment中的log
log="/publish/logs/"
// ------ 以下内容,一般所有的项目都一样,不经常修改 ------
// harbor inner address
repoHost = "192.168.0.1:23280"
// harbor的账号密码信息,在jenkins中配置用户名/密码形式的认证信息,命名成harbor即可
harborCreds = credentials('harbor')
// filebeat的镜像地址
filebeatImage="${repoHost}/common/filebeat:6.3.1"
// es的内网访问地址
es="elasticsearch-logging.kube-system:9200"
}
// ------ 以下内容无需修改 ------
stages {
// 开始构建前清空工作目录
stage ("CleanWS"){
steps {
script {
try{
deleteDir()
}catch(err){
echo "${err}"
sh 'exit 1'
}
}
}
}
// 拉取
stage ("CheckOut"){
steps {
script {
try{
checkout([$class: 'GitSCM', branches: [[name: "*/${branch}"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'gitlab', url: "${git}"]]])
}catch(err){
echo "${err}"
sh 'exit 1'
}
}
}
}
// 构建
stage ("Build"){
steps {
script {
try{
// 登录 harbor
sh "docker login -u ${harborCreds_USR} -p ${harborCreds_PSW} ${repoHost}"
sh "date +%Y%m%d%H%m%S > timestamp"
// 镜像tag用时间戳代表
tag = readFile('timestamp').replace("\n", "").replace("\r", "")
repoPath = "${repoHost}/${namespace}/${app}:${tag}"
// 根据分支,进入_deploy下对应的不同文件夹,通过dockerfile打包镜像
sh "cp _deploy/${branch}/* ./"
sh "docker login -u ${harborCreds_USR} -p ${harborCreds_PSW} ${repoHost}"
sh "docker build -t ${repoPath} ."
}catch(err){
echo "${err}"
sh 'exit 1'
}
}
}
}
// 镜像推送到harbor
stage ("Push"){
steps {
script {
try{
sh "docker push ${repoPath}"
}catch(err){
echo "${err}"
sh 'exit 1'
}
}
}
}
// 使用pipeline script中复制的变量替换deployment.yaml中的占位变量,执行deployment.yaml进行部署
stage ("Deploy"){
steps {
script {
try{
sh "sed -i 's|#namespace#|${namespace}|g' deployment.yaml"
sh "sed -i 's|#app#|${app}|g' deployment.yaml"
sh "sed -i 's|#image#|${repoPath}|g' deployment.yaml"
sh "sed -i 's|#port#|${port}|g' deployment.yaml"
sh "sed -i 's|#host#|${host}|g' deployment.yaml"
sh "sed -i 's|#replicas#|${replicas}|g' deployment.yaml"
sh "sed -i 's|#log#|${log}|g' deployment.yaml"
sh "sed -i 's|#filebeatImage#|${filebeatImage}|g' deployment.yaml"
sh "sed -i 's|#es#|${es}|g' deployment.yaml"
sh "sed -i 's|#redisImage#|${redisImage}|g' deployment.yaml"
sh "cat deployment.yaml"
sh "kubectl apply -f deployment.yaml"
}catch(err){
echo "${err}"
sh 'exit 1'
}
}
}
}
}
post {
// 使用钉钉插件进行通知
always {
script {
def msg = "【${author}】你把服务器搞挂了,老詹喊你回家改BUG!"
def imageUrl = "https://www.iconsdb.com/icons/preview/red/x-mark-3-xxl.png"
if (currentBuild.currentResult=="SUCCESS"){
imageUrl= "http://icons.iconarchive.com/icons/paomedia/small-n-flat/1024/sign-check-icon.png"
msg ="【${author}】发布成功,干得不错!"
}
dingTalk accessToken:"xxxx",message:"${msg}",imageUrl:"${imageUrl}",messageUrl:"${BUILD_URL}"
}
}
}
}

发布完成后,可以参考《持续集成CI实施指南三–jenkins集成测试》,做持续测试,测试结果也可通过钉钉通知。最后我们利用自建的运维平台,监控阿里云ECS状态、K8S各组件状态、监控ES中的日志并做异常抓取和报警。形成一整套DevOps模式。

综上,对于每个项目,我们只需维护Dockerfile,并在Jenkins创建持续集成项目时,填写项目所需的参数变量。进阶情况下,也可定制性的修改deployment文件与pipeline script,满足不同的业务需要。至此,完结,撒花!

评论和共享

目录

  1. 一、Jenkins
    1. 1.1 准备镜像
    2. 1.2 部署Jenkins Master
    3. 1.3 配置Jenkins Slave
    4. 1.4 测试验证
  2. 二、K8S资源管理
  3. 三、Harbor
  4. 四、EFK

前言:

在系列的第一篇文章中,我已经介绍过如何在阿里云基于kubeasz搭建K8S集群,通过在K8S上部署gitlab并暴露至集群外来演示服务部署与发现的流程。文章写于4月,忙碌了小半年后,我才有时间把后续部分补齐。系列会分为三篇,本篇将继续部署基础设施,如jenkins、harbor、efk等,以便为第三篇项目实战做好准备。

需要说明的是,阿里云迭代的实在是太快了,2018年4月的时候,由于SLB不支持HTTP跳转HTTPS,迫不得已使用了Ingress-Nginx来做跳转控制。但在4月底的时候,SLB已经在部分地区如华北、国外节点支持HTTP跳转HTTPS。到了5月更是全节点支持。这样以来,又简化了Ingress-Nginx的配置。

1

一、Jenkins

一般情况下,我们搭建一个Jenkins用于持续集成,那么所有的Jobs都会在这一个Jenkins上进行build,如果Jobs数量较多,势必会引起Jenkins资源不足导致各种问题出现。于是,对于项目较多的部门、公司使用Jenkins,需要搭建Jenkins集群,也就是增加Jenkins Slave来协同工作。

但是增加Jenkins Slave又会引出新的问题,资源不能按需调度。Jobs少的时候资源闲置,而Jobs突然增多仍然会资源不足。我们希望能动态分配Jenkins Slave,即用即拿,用完即毁。这恰好符合K8S中Pod的特性。所以这里,我们在K8S中搭建一个Jenkins集群,并且是Jenkins Slave in Pod.

1.1 准备镜像

我们需要准备两个镜像,一个是Jenkins Master,一个是Jenkins Slave:

Jenkins Master

可根据实际需求定制Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FROM jenkins/jenkins:latest
USER root
# Set jessie source
RUN cecho '' > /etc/apt/sources.list.d/jessie-backports.list \
&& echo "deb http://mirrors.aliyun.com/debian jessie main contrib non-free" > /etc/apt/sources.list \
&& echo "deb http://mirrors.aliyun.com/debian jessie-updates main contrib non-free" >> /etc/apt/sources.list \
&& echo "deb http://mirrors.aliyun.com/debian-security jessie/updates main contrib non-free" >> /etc/apt/sources.list
# Update
RUN apt-get update && apt-get install -y libltdl7 && apt-get clean
# INSTALL KUBECTL
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl && \
chmod +x ./kubectl && \
mv ./kubectl /usr/local/bin/kubectl
# Set time zone
RUN rm -rf /etc/localtime && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo 'Asia/Shanghai' > /etc/timezone
# Skip setup wizard、 TimeZone and CSP
ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false -Duser.timezone=Asia/Shanghai -Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';\""

Jenkins Salve

一般来说只需要安装kubelet就可以了

1
2
3
4
5
6
7
8
FROM jenkinsci/jnlp-slave
USER root
# INSTALL KUBECTL
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl && \
chmod +x ./kubectl && \
mv ./kubectl /usr/local/bin/kubectl

生成镜像后可以push到自己的镜像仓库中备用

1.2 部署Jenkins Master

为了部署Jenkins、Jenkins Slave和后续的Elastic Search,建议ECS的最小内存为8G

在K8S上部署Jenkins的yaml参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
apiVersion: v1
kind: Namespace
metadata:
name: jenkins-ci
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins-ci
namespace: jenkins-ci
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: jenkins-ci
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: jenkins-ci
namespace: jenkins-ci
---
# 设置两个pv,一个用于作为workspace,一个用于存储ssh key
apiVersion: v1
kind: PersistentVolume
metadata:
name: jenkins-home
labels:
release: jenkins-home
namespace: jenkins-ci
spec:
# workspace 大小为10G
capacity:
storage: 10Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
# 使用阿里云NAS,需要注意,必须先在NAS创建目录 /jenkins/jenkins-home
nfs:
path: /jenkins/jenkins-home
server: xxxx.nas.aliyuncs.com
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: jenkins-ssh
labels:
release: jenkins-ssh
namespace: jenkins-ci
spec:
# ssh key 只需要1M空间即可
capacity:
storage: 1Mi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
# 不要忘了在NAS创建目录 /jenkins/ssh
nfs:
path: /jenkins/ssh
server: xxxx.nas.aliyuncs.com
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins-home-claim
namespace: jenkins-ci
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
selector:
matchLabels:
release: jenkins-home
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins-ssh-claim
namespace: jenkins-ci
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Mi
selector:
matchLabels:
release: jenkins-ssh
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: jenkins
namespace: jenkins-ci
spec:
replicas: 1
template:
metadata:
labels:
name: jenkins
spec:
serviceAccount: jenkins-ci
containers:
- name: jenkins
imagePullPolicy: Always
# 使用1.1小结创建的 Jenkins Master 镜像
image: xx.xx.xx/jenkins:1.0.0
# 资源管理,详见第二章
resources:
limits:
cpu: 1
memory: 2Gi
requests:
cpu: 0.5
memory: 1Gi
# 开放8080端口用于访问,开放50000端口用于Jenkins Slave和Master的通讯
ports:
- containerPort: 8080
- containerPort: 50000
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 40
periodSeconds: 20
securityContext:
privileged: true
volumeMounts:
# 映射K8S Node的docker,也就是docker outside docker,这样就不需要在Jenkins里面安装docker
- mountPath: /var/run/docker.sock
name: docker-sock
- mountPath: /usr/bin/docker
name: docker-bin
- mountPath: /var/jenkins_home
name: jenkins-home
- mountPath: /root/.ssh
name: jenkins-ssh
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
- name: docker-bin
hostPath:
path: /opt/kube/bin/docker
- name: jenkins-home
persistentVolumeClaim:
claimName: jenkins-home-claim
- name: jenkins-ssh
persistentVolumeClaim:
claimName: jenkins-ssh-claim
---
kind: Service
apiVersion: v1
metadata:
name: jenkins-service
namespace: jenkins-ci
spec:
type: NodePort
selector:
name: jenkins
# 将Jenkins Master的50000端口作为NodePort映射到K8S的30001端口
ports:
- name: jenkins-agent
port: 50000
targetPort: 50000
nodePort: 30001
- name: jenkins
port: 8080
targetPort: 8080
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: jenkins-ingress
namespace: jenkins-ci
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
spec:
rules:
# 设置Ingress-Nginx域名和端口
- host: xxx.xxx.com
http:
paths:
- path: /
backend:
serviceName: jenkins-service
servicePort: 8080

最后附一下SLB的配置

2

这样就可以通过域名xxx.xxx.com访问Jenkins,并且可以通过xxx.xxx.com:50000来链接集群外的Slave。当然,集群内的Slave直接通过serviceName-namespace:50000访问就可以了

1.3 配置Jenkins Slave

以管理员进入Jenkins,安装”Kubernetes”插件,然后进入系统设置界面,”Add a new cloud” - “Kubernetes”,配置如下:

  • Kubernetes URL:https://kubernetes.default.svc.cluster.local

  • Jenkins URL:http://jenkins-service.jenkins-ci:8080

    3

  • Test Connection 测试看连接是否成功

  • Images - Add Pod Template - Kubernetes Pod Template

  • 注意设置Name为”jnlp-agent”,其他按需填写,设置完成后进入Advanced

    4

  • 根据需要设置资源管理,也就是说限制Jenkins Slave in Pod所占用的CPU和内存,详见第二章

    5

  • 设置Volume,同样采用docker outside docker,将K8S Node的docker为Jenkins Slave Pod所用;设置Jenkins Slave的工作目录为NAS

    6

  • 设置最多允许多少个Jenkins Slave Pod 同时运行,然后进入Advanced

    7

  • 填写Service Account,与部署Jenkins Master的yaml文件中的Service Account保持一致;如果你的Jenkins Slave Image是私有镜像,还需要设置ImagePullSecrets

    8

  • Apply并完成

1.4 测试验证

我们可以写一个FreeStyle Project的测试Job:

9

测试运行:

可以看到名为”jnlp-agent-xxxxx”的Jenkins Salve被创建,Job build完成后又消失,即为正确完成配置。

10

11

二、K8S资源管理

在第一章中,先后提到两次资源管理,一次是Jenkins Master的yaml,一次是Kubernetes Pod Template给Jenkins Slave 配置。Resource的控制是K8S的基础配置之一。但一般来说,用到最多的就是以下四个:

  • Request CPU:意为某Node剩余CPU大于Request CPU,才会将Pod创建到该Node上
  • Limit CPU:意为该Pod最多能使用的CPU为Limit CPU
  • Request Memory:意为某Node剩余内存大于Request Memory,才会将Pod创建到该Node上
  • Limit Memory:意为该Pod最多能使用的内存为Limit Memory

比如在我这个项目中,Gitlab至少需要配置Request Memory为3G,对于Elastic Search的Request Memory也至少为2.5 G.

其他服务需要根据K8S Dashboard中的监控插件结合长时间运行后给出一个合理的Resource控制范围。

12

13

三、Harbor

在K8S中跑CI,大致流程是Jenkins将Gitlab代码打包成Image,Push到Docker Registry中,随后Jenkins通过yaml文件部署应用,Pod的Image从Docker Registry中Pull.也就是说到目前为止,我们还缺一个Docker Registry才能准备好所有CI需要的基础软件。

利用阿里云的镜像仓库或者Docker HUB可以节省硬件成本,但考虑数据安全、传输效率和操作易用性,还是希望自建一个Docker Registry. 可选的方案并不多,官方提供的Docker Registry v2轻量简洁,vmware的Harbor功能更丰富。

Harbor提供了一个界面友好的UI,支持镜像同步,这对于DevOps尤为重要。Harbor官方提供了Helm方式在K8S中部署。但我考虑Harbor占用的资源较多,从节省硬件成本来说,把Harbor放到了K8S Master上(Master节点不会被调度用于部署Pod,所以大部分空间资源没有被利用)。当然这不是一个最好的方案,但它是最适合我们目前业务场景的方案。

在Master节点使用docker compose部署Harbor的步骤如下:

  • 192.168.0.1安装docker-compose

    1
    pip install docker-compose
  • 192.168.0.1 data目录挂载NAS路径(harbor的volume默认映射到宿主机的/data目录,所以我们把宿主机的/data目录挂载为NAS即可实现用NAS作为harbor的volume)

    1
    2
    mkdir /data
    mount -t nfs -o vers=4.0 xxx.xxx.com:/harbor /data
  • 参考https://github.com/vmware/harbor/blob/master/docs/installation_guide.md 安装

    • 根据需要下载指定版本的 Harbor offline installer

    • 解压后配置harbor.cfg

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      # 域名
      hostname = xx.xx.com
      # 协议,这里可以使用http可以免去配置ssl_cert,通过SLB暴露至集群外再加上ssh即可
      ui_url_protocol = http
      # 邮箱配置
      email_identity = rfc2595
      email_server = xx
      email_server_port = xx
      email_username = xx
      email_password = xx
      email_from = xx
      email_ssl = xx
      email_insecure = xx
      # admin账号默认密码
      harbor_admin_password = xx
    • 修改docker-compose.yaml中的端口映射,这里将容器端口映射到宿主机的23280端口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      proxy:
      image: vmware/nginx-photon:v1.5.0
      container_name: nginx
      restart: always
      volumes:
      - ./common/config/nginx:/etc/nginx:z
      networks:
      - harbor
      ports:
      - 23280:80
      #- 443:443
      #- 4443:4443
      depends_on:
      - mysql
      - registry
      - ui
      - log
      logging:
      driver: "syslog"
      options:
      syslog-address: "tcp://127.0.0.1:1514"
      tag: "proxy"
    • 运行install.sh

  • 修改 kubeasz的 roles/docker/files/daemon.json加入”insecure-registries”节点,如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    "registry-mirrors": ["https://kuamavit.mirror.aliyuncs.com", "https://registry.docker-cn.com", "https://docker.mirrors.ustc.edu.cn"],
    "insecure-registries": ["192.168.0.1:23280"],
    "max-concurrent-downloads": 10,
    "log-driver": "json-file",
    "log-level": "warn",
    "log-opts": {
    "max-size": "10m",
    "max-file": "3"
    }
    }

    重新安装kubeasz的docker

    1
    ansible-playbook 03.docker.yml

    这样在集群内的任何一个节点就可以通过http协议192.168.0.1:23280 访问harbor

  • 开机启动

    1
    2
    3
    4
    5
    6
    vi /etc/rc.local
    # 加入如下内容
    # mount -t nfs -o vers=4.0 xxxx.com:/harbor /data
    # cd /etc/ansible/heygears/harbor
    # sudo docker-compose up -d
    chmod +x /etc/rc.local
  • 设置Secret(K8S部署应用时使用Secret拉取镜像,详见系列教程第三篇)

    在K8S集群任意一台机器使用命令

    1
    kubectl create secret docker-registry regcred --docker-server=192.168.0.1:23280 --docker-username=xxx --docker-password=xxx --docker-email=xxx
  • 设置SLB(如果仅在内网使用,不设置SLB和DNS也可以)

    14

  • 登陆Harbor管理页面

  • 在集群内通过docker login 192.168.0.1:23280验证Harbor是否创建成功

四、EFK

最后我们来给集群加上日志系统。

项目中常用的日志系统多数是Elastic家族的ELK,外加Redis或者Kafka作为缓冲队列。由于Logstash需要运行在java环境下,且占用空间大,配置相对复杂,随着Elastic家族的产品逐渐丰富,Logstash开始慢慢偏向日志解析、过滤、格式化等方面,所以并不太适合在容器环境下的日志收集。K8S官方给出的方案是EFK,其中F指的是Fluentd,一个用Ruby写的轻量级日志收集工具。对比Logstash来说,支持的插件少一些。

容器日志的收集方式不外乎以下四种:

  • 容器外收集。将宿主机的目录挂载为容器的日志目录,然后在宿主机上收集。
  • 容器内收集。在容器内运行一个后台日志收集服务。
  • 单独运行日志容器。单独运行一个容器提供共享日志卷,在日志容器中收集日志。
  • 网络收集。容器内应用将日志直接发送到日志中心,比如java程序可以使用log4j2转换日志格式并发送到远端。
  • 通过修改docker的–log-driver。可以利用不同的driver把日志输出到不同地方,将log-driver设置为syslog、fluentd、splunk等日志收集服务,然后发送到远端。

docker默认的driver是json-driver,容器输出到控制台的日志,都会以 *-json.log 的命名方式保存在 /var/lib/docker/containers/ 目录下。所以EFK的日志策略就是在每个Node部署一个Fluentd,读取/var/lib/docker/containers/ 目录下的所有日志,传输到ES中。这样做有两个弊端,一方面不是所有的服务都会把log输出到控制台;另一方面不是所有的容器都需要收集日志。我们更想定制化的去实现一个轻量级的日志收集。所以综合各个方案,还是采取了网上推荐的以FileBeat作为日志收集的“EFK”架构方案。

FileBeat用Golang编写,输出为二进制文件,不存在依赖。占用空间极小,吞吐率高。但它的功能相对单一,仅仅用来做日志收集。所以对于有需要的业务场景,可以用FileBeat收集日志,Logstash格式解析,ES存储,Kibana展示。

使用FileBeat收集容器日志的业务逻辑如下:

15

也就是说我们利用K8S的Pod的临时目录{}来实现Container的数据共享,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: test
labels:
app: test
spec:
replicas: 2
strategy:
type: Recreate
template:
metadata:
labels:
app: test
spec:
containers:
- image: #appImage
name: app
volumeMounts:
- name: log-volume
mountPath: /var/log/app/ #app log path
- image: #filebeatImage
name: filebeat
args: [
"-c", "/etc/filebeat.yml"
]
securityContext:
runAsUser: 0
volumeMounts:
- name: config
mountPath: /etc/filebeat.yml
readOnly: true
subPath: filebeat.yml
- name: log-volume
mountPath: /var/log/container/
volumes:
- name: config
configMap:
defaultMode: 0600
name: filebeat-config
- name: log-volume
emptyDir: {} #利用{}实现数据交互
imagePullSecrets:
- name: regcred
---
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
namespace: test
labels:
app: filebeat
data:
filebeat.yml: |-
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/container/*.log #FileBeat读取log的源
output.elasticsearch:
hosts: ["xx.xx.xx:9200"]
tags: ["test"] #log tag

实现这种FileBeat作为日志收集的“EFK”系统,只需要在K8S集群中搭建好ES和Kibana即可,FileBeat是随着应用一起创建,无需提前部署。搭建ES和Kibana的方式可参考K8S官方文档,我也进行了一个简单整合:

ES:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# RBAC authn and authz
apiVersion: v1
kind: ServiceAccount
metadata:
name: elasticsearch-logging
namespace: kube-system
labels:
k8s-app: elasticsearch-logging
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: elasticsearch-logging
labels:
k8s-app: elasticsearch-logging
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
rules:
- apiGroups:
- ""
resources:
- "services"
- "namespaces"
- "endpoints"
verbs:
- "get"
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: kube-system
name: elasticsearch-logging
labels:
k8s-app: elasticsearch-logging
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
subjects:
- kind: ServiceAccount
name: elasticsearch-logging
namespace: kube-system
apiGroup: ""
roleRef:
kind: ClusterRole
name: elasticsearch-logging
apiGroup: ""
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: es-pv-0
labels:
release: es-pv
namespace: kube-system
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteMany
volumeMode: Filesystem
persistentVolumeReclaimPolicy: Recycle
storageClassName: "es-storage-class"
nfs:
path: /es/0
server: xxx.nas.aliyuncs.com # 用NAS来作为ES的数据存储,需要提前在NAS创建目录/es/0
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: es-pv-1
labels:
release: es-pv
namespace: kube-system
spec:
capacity:
storage: 20Gi
accessModes:
- ReadWriteMany
volumeMode: Filesystem
persistentVolumeReclaimPolicy: Recycle
storageClassName: "es-storage-class"
nfs:
path: /es/1
server: xxx.nas.aliyuncs.com # 用NAS来作为ES的数据存储,需要提前在NAS创建目录/es/1
---
# Elasticsearch deployment itself
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch-logging
namespace: kube-system
labels:
k8s-app: elasticsearch-logging
version: v5.6.4
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
serviceName: elasticsearch-logging
replicas: 2
selector:
matchLabels:
k8s-app: elasticsearch-logging
version: v5.6.4
template:
metadata:
labels:
k8s-app: elasticsearch-logging
version: v5.6.4
kubernetes.io/cluster-service: "true"
spec:
serviceAccountName: elasticsearch-logging
containers:
- image: registry-vpc.cn-shenzhen.aliyuncs.com/heygears/elasticsearch:5.6.4 # 可替换成私有仓库
name: elasticsearch-logging
resources:
# need more cpu upon initialization, therefore burstable class
limits:
cpu: 1
memory: 2.5Gi
requests:
cpu: 0.8
memory: 2Gi
ports:
- containerPort: 9200
name: db
protocol: TCP
- containerPort: 9300
name: transport
protocol: TCP
volumeMounts:
- name: elasticsearch-logging
mountPath: /data
env:
- name: "NAMESPACE"
valueFrom:
fieldRef:
fieldPath: metadata.namespace
# Elasticsearch requires vm.max_map_count to be at least 262144.
# If your OS already sets up this number to a higher value, feel free
# to remove this init container.
initContainers:
- image: alpine:3.6
command: ["/sbin/sysctl", "-w", "vm.max_map_count=262144"]
name: elasticsearch-logging-init
securityContext:
privileged: true
volumeClaimTemplates:
- metadata:
name: elasticsearch-logging
spec:
accessModes: [ "ReadWriteMany" ]
storageClassName: "es-storage-class"
resources:
requests:
storage: 20Gi
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-logging
namespace: kube-system
labels:
k8s-app: elasticsearch-logging
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/name: "Elasticsearch"
spec:
type: NodePort
ports:
- port: 9200
protocol: TCP
targetPort: db
nodePort: xxx # 以NodePort方式暴露端口,供集群外访问ES
selector:
k8s-app: elasticsearch-logging

Kibana:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
apiVersion: apps/v1
kind: Deployment
metadata:
name: kibana-logging
namespace: kube-system
labels:
k8s-app: kibana-logging
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
spec:
replicas: 1
selector:
matchLabels:
k8s-app: kibana-logging
template:
metadata:
labels:
k8s-app: kibana-logging
spec:
containers:
- name: kibana-logging
image: registry-vpc.cn-shenzhen.aliyuncs.com/heygears/kibana:5.6.4 # 也可替换成自己的私有仓库
resources:
# need more cpu upon initialization, therefore burstable class
limits:
cpu: 1
memory: 1.5Gi
requests:
cpu: 0.8
memory: 1.5Gi
env:
- name: ELASTICSEARCH_URL
value: http://elasticsearch-logging:9200
- name: SERVER_BASEPATH
value: /api/v1/namespaces/kube-system/services/kibana-logging/proxy
- name: XPACK_MONITORING_ENABLED
value: "false"
- name: XPACK_SECURITY_ENABLED
value: "false"
ports:
- containerPort: 5601
name: ui
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: kibana-logging
namespace: kube-system
labels:
k8s-app: kibana-logging
kubernetes.io/cluster-service: "true"
addonmanager.kubernetes.io/mode: Reconcile
kubernetes.io/name: "Kibana"
spec:
ports:
- port: 5601
protocol: TCP
targetPort: ui
selector:
k8s-app: kibana-logging

评论和共享

Docker 入门教程

发布在 virtualization

该教程仅对docker常用操作进行了总结归纳

目录

  1. 一、 什么是Docker
    1. 1.1 什么是LXC
    2. 1.2 什么是容器
    3. 1.3 容器和虚拟机的区别
  2. 二、 Docker的3个概念
    1. 2.1 Image
    2. 2.2 Container
    3. 2.3 Registry
  3. 三、 Docker 版本与安装
  4. 四、 Docker常用指令
    1. 4.1 Image 指令
    2. 4.2 Container 指令
    3. 4.3 Registry 指令
    4. 4.4 练习
  5. 五、 Docker Volume
  6. 六、 容器网络
    1. 6.1 访问容器
    2. 6.2 端口映射
    3. 6.3 容器间通讯
  7. 七、 Dockerfile
    1. 7.1 Dockerfile编写方式
    2. 7.2 Dockerfile构建方式
  8. 八、 Docker Compose

一、 什么是Docker

Docker是2013年,dotCloud用Golang开发的基于LXC的高级容器引擎

1.1 什么是LXC

Linux Container的简写。可以提供轻量级的虚拟化,以便隔离进程和资源,而且不需要提供指令解释机制以及全虚拟化的其他复杂性。相当于C++中的NameSpace

1.2 什么是容器

是一个相对独立的运行环境

1.3 容器和虚拟机的区别

1

Docker 虚拟机
启动速度 秒级 分钟级
启动方式 启动应用 启动Guest OS + 启动应用
交付、部署 Docker镜像(Dockerfile) 虚拟机镜像
更新 修改Dockerfile或发布新镜像 向虚拟机推送补丁升级包
操作系统 Linux x64 Linux、Windows、Mac OS等
磁盘占用率
性能利用率
成熟度 发展中
稳定性 发展中
隔离性
数据保留 删除后销毁 删除后销毁

二、 Docker的3个概念

Docker Git OOD
Image Repository Code Class
Container Local Code Instance
Registry GitHub/GitLab

2.1 Image

  • 镜像,Docker Image是一系列操作的集合,每一组操作形成一个layer,每个layer有一个id,并说明该layer依赖的前一个layer的id,多个layer堆叠形成Image,注意理解一组操作的意义

    18

    2

  • Image是静态的,不能直接使用。像面向对象中的类,也像Git中放在仓库中的代码

  • 可以给Image设置Repository,同一个Image ID可以有多个Repository,Repository的命名规范是“Registry Name/Image Name”,若Registry Name为空,则默认Registry为Docker HUB3

  • 可以给Image设置Tag,同一个Image ID可以有多个标签Tag,若tag为空,则默认Tag为latest,表示最新的镜像4

  • Image可以在本地通过Container Commit生成,亦可用Dockerfile生成,还可以从Registry Pull下来

2.2 Container

  • 容器,Docker Container 是Image的实例化对象
  • Container是动态的,可以使用的。像面向对象中的对象,也像Git中被pull到本地的代码,而且代码可以编译和运行
  • Container经过一系列操作后,并不会影响Image,如果需要生成新的Image,需要做类似git的流程对Container进行Commit操作
  • Container删除会导致Container中的数据消失,从Image启动新的Container是一个全新的容器,不能发现或共享其他Container中的数据5

2.3 Registry

  • 仓库,Docker Registry 是存放Image的地方
  • Registry像GitHub、Gitlab一样,有官方提供的Docker HUB,也可以自己搭建
  • Registry里面的项目有公开和私有两种权限,公开项目下的镜像可以被任何人pull,私有项目的镜像只能被Registry分配权限的用户pull6

三、 Docker 版本与安装

7

8

四、 Docker常用指令

4.1 Image 指令

  • docker images

    查询所有镜像

    9

  • docker tag

    “复制”一个Image副本,并重新命名(包括Repository Name、Image Name和Tag)

    • docker tag f06 my/redis:4

      f06a5773f01e 可以简写开头的几位

    • docker tag redis:latest redis:4

    • docker tag redis my/redis

      表示把redis:latest 复制为 my/redis:latest

  • docker rmi

    删除指定的镜像

    10

    • docker rmi redis:4

      删除redis:4 这个Image,若该Image ID在本地有其他名称或Tag,则只移除这个Tag

    • docker rmi f06

      删除Image ID缩写为f06的 Image,该Image ID的所有副本(不同的Repository、Image Name和Tag)都会被删除

    • docker rmi -f f06

      默认情况下,rmi指令只能删除没有Container的Image;通过-f 参数可以强制删除有Container但Container是停止状态的Image;不能直接删除有运行状态Container的Image,必须先停止、删除Container后再删除Image

  • docker history

    查询指定镜像的操作历史

    11

    • docker history f06

      查看镜像的所有历史操作,甚至可以通过该指令看出镜像是如何构建出来的,但对于Container的操作在这里无法体现

  • docker inspect

    查询指定镜像的详细信息

    12

    • docker inspect f06

4.2 Container 指令

  • docker run

    从Image运行一个新Container

    • docker run --name myRedis f06

      从Image运行容器并通过–name 命名为myRedis

    • docker run -it f06 bash

      从Image运行容器,随机名称,-it 是 -i和-t,表示交互并重定向到控制台,bash表示容器入口,不填则进入镜像的默认入口

    • docker run -d f06

      -d 表示后台执行

  • docker ps

    查看容器列表

    13

    • docker ps

      查看仅在运行状态的Container

    • docker ps -a

      查看所有状态的Container

  • docker inspect

    查看指定容器的详细信息

    • docker inspect myRedis
  • docker exec

    14

    进入指定的容器

    • docker exec -it myRedis bash

      以交互模式进入myRedis 容器的bash

    • docker exec -u root -it myRedis bash

      同上,但以root用户登录

  • docker stop/start/restart

    停止/启动/重启容器

    • docker stop myRedis
    • docker start myRedis
    • docker restart myRedis
  • docker rm

    删除指定的容器

    • docker rm myRedis

      删除名为myRedis 且状态为退出、停止的容器

    • docker rm 3c7

      删除ID简写为3c7且状态为退出、停止的容器

    • docker rm -f myRedis

      强制删除名为myRedis 的容器,不管容器是否正在运行

4.3 Registry 指令

  • docker login

    登录指定的docker registry

    • docker login -u test -p xxx hub.docker.com

      以用户test,密码为xxx登录Docker HUB

  • docker pull

    从指定的docker registry pull指定的Image,对于私有镜像,必须先用docker login登录registry

    • docker pull test/redis:4

      从Docker HUB的test用户拉取redis:4 镜像

    • docker pull a.b.com/test/redis:4

      从a.b.com这个registry的test用户拉取redis:4 镜像

  • docker push

    把指定Image push到docker registry,必须先用docker login登录registry

    • docker tag redis:latest a.b.com/test/common/redis:4

    • docker push a.b.com/test/common/redis:4

      把redis:lastet复制为a.b.com/test/common/redis:4,并push到a.b.com的test用户的common项目下,镜像名为redis,Tag为4

4.4 练习

  • 从Docker HUB pull一个Nginx Image
  • 运行Nginx Container,并访问默认页面
  • 进入容器删除 /usr/share/nginx/html/index.html ,提交容器到新镜像
  • 关闭容器,从旧镜像启动Nginx Container,访问页面并观察
  • 关闭容器,从新镜像启动Nginx Container,访问页面并观察
  • 注册Docker HUB账号,将新镜像提交至自己的Docker HUB

五、 Docker Volume

由于Docker Container释放后不能保存数据,并且Container之间不能共享数据,所以需要“外接一个存储”用于数据的持久化和共享。这就是挂载卷Volume

PS:因为非Linux环境的Docker都是基于Linux虚拟机,所以在非Linux环境下使用Docker Volume需注意以下几点:

  • 如果使用DockerToolBox,需要给虚拟机挂载共享目录,只有这个目录可以作为Volume
  • 如果使用https://github.com/sonicrang/Docker_FrontEnd 我已经在控制台封装好设置虚拟机共享目录的功能,方法为控制台输入1,即可将本机目录例如”e:\test”挂到虚拟机”/develop”下,只有”/develop”目录可以作为Volume

挂载示例:

  • docker run -v /develop/nginx/log:/var/log/nginx --name myNginx nginx

    将宿主机的”/develop/nginx/log” 空目录挂载到容器的”/var/log/nginx”,启动容器后,对于linux系统,可以在宿主机”/develop/nginx/log”看到容器内的日志文件;对于使用DockerToolBox或者Docker_FrontEnd,会在本机的”e:\test”看到容器中的内容。当容器删除后,宿主机目录中的日志文件会被保留下来

    15

  • docker run -v nginx:/var/log/nginx --name myNginx nginx

    将宿主机的空目录”/var/lib/docker/volumn/nginx” 挂载到容器的”/var/log/nginx”,也就是说宿主机的目录可以填写为相对目录,相对于一个docker的默认安装目录

  • docker run -v /var/log/nginx --name myNginx nginx

    将宿主机的空目录”/var/lib/docker/volumn/containerID” 挂载到容器的”/var/log/nginx”,也就是说宿主机的目录可以不填,默认为docker安装目录对应的containerID目录

  • docker run -v /develop/nginx/html:/usr/share/nginx/html --name myNginx nginx

    注意在之前的例子中,我们把空目录挂载到容器的空文件夹中(因为log是运行容器后才产生的)。但在这个例子中,我们把空目录挂载到容器的非空文件夹”/usr/share/nginx/html”,由于挂载源是空文件夹,会覆盖掉挂载目标也就是”/usr/share/nginx/html”的内容,这样就导致nginx的html文件丢失,所以切记不要用一个空目录挂载到容器在运行前的非空目录下,如果我们要在挂载源控制nginx的html文件内容,那就必须要保证挂载源非空且存在html文件

  • docker run -v /develop/nginx/html/index.html:/usr/share/nginx/html/index.html:ro --name myNginx nginx

    将宿主机的”/develop/nginx/html/index.html”文件以只读的形式挂载到容器的”/usr/share/nginx/html/index.html”

六、 容器网络

6.1 访问容器

  • 对于linux下的docker,本机IP+端口就可以访问容器,比如127.0.0.1就可以访问nginx容器
  • 对于DockerToolBox和Docker_FrontEnd,要访问虚拟机的IP+端口,默认为192.168.99.100

但是此时输入URL还不可以访问容器,因为端口还没有映射

6.2 端口映射

  • docker run -p 80:80 --name myNginx nginx

    把容器的80端口映射到宿主的80端口,这样就可通过宿主机IP:80访问nginx容器

  • docker run -p 8080:80 --name myNginx nginx

    把容器的80端口映射到宿主的8080端口,这样就可通过宿主机IP:8080访问nginx容器

  • docker run -p 8080:80 -p 8081:81 --name myNginx nginx

    把容器的80端口映射到宿主的8080端口,同时把容器81端口映射到宿主的8081端口

  • docker run -p 8080-8085:80-85 --name myNginx nginx

    把容器的80、81、82、83、84、85端口映射到宿主的8080、8081、8082、8083、8084、8085端口

  • docker run -P --name myNginx nginx

    把容器的所有开放端口随机映射到宿主

6.3 容器间通讯

相同宿主机下有两个容器A、B

容器A内部端口80映射到宿主的8080

容器B访问容器A的方法如下:

  • 宿主IP:8080

    curl 192.168.99.100:8080

  • 容器A IP:80

    curl 172.17.0.2

    容器A的IP获取需要使用命令docker inspect A

    16

七、 Dockerfile

在2.1小结我们提到Image其实是一系列操作的层堆叠起来。但是传统的commit方式不能很好的呈现Image是如何构建的。试想一个场景,搭建一个软件运行环境的Image,这个Image会随着业务、时间不断更新,如果运维人员想看Image究竟是怎样堆叠起来的,只能用history指令,不利于查看为维护。所以这又引出了2.1小结提到的另一个概念Dockerfile

7.1 Dockerfile编写方式

下面的示例是将一个golang程序打包成Image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# FROM关键字 说明原始镜像
FROM golang:1.9.2-alpine3.6
# RUN关键字用于执行命令
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep
# COPY Dockfile上下文目录的Gopkg.lock Gopkg.toml 到镜像的/go/src/project目录下,注意如果是目录,最后不要忘了/
COPY Gopkg.lock Gopkg.toml /go/src/project/
# WORKDIR 关键字相当于切换目录
WORKDIR /go/src/project/
RUN dep ensure -vendor-only
# COPY Dockfile上下文目录的所有文件到镜像的/go/src/project目录下
COPY . /go/src/project/
RUN go build -o /bin/project
# 暴露端口80,参考6.2小结,如果使用-P映射(大写P),用以告诉docker,容器的80端口是开放的,可以映射出去
EXPOSE 80
# 设置入口,相当于通过镜像启动容器后,进入"/bin/project",该配置可以被docker run --entrypoint xxx app 强行覆盖
ENTRYPOINT ["/bin/project"]
# 执行命令,该配置可以被docker run app xxx 强行覆盖
CMD ["--help"]

什么是Dockerfile的上下文?需要了解Dockerfile的构建方式

7.2 Dockerfile构建方式

  • docker build -t myRepo/myAPP:latest .

    注意最后的那个点”.”,查找当前目录名为Dockerfile的文件,构建Image并命名为myRepo/myAPP:latest

  • docker build -f myRepo/myAPP:latest -f myDockerfile

    查找当前目录名为myDockerfile的文件,构建Image并命名为myRepo/myAPP:latest

在上面两个例子中,Dockfile的上下文就是指Dockerfile文件所在的目录,先看下图:

17

左右两幅图目录结构不同,如果Dockerfile具有同样的内容:

1
2
FROM XXX
COPY . /target

那么左边的目录结构,最终被放到镜像”/target”目录中的文件应该只有”run.sh”这个文件,而右边的目录结构,最后在”/target”中的是”document”和”source_code”的文件夹及内容。

八、 Docker Compose

试想一个业务场景,我们有一个服务容器A需要连接数据库,把数据库装到A的镜像里不是一个明智的选择,一般情况下我们会再搭建一个容器B作为数据库,那么这时,两个容器如何关联?这里的关联不仅包括如何保证两个容器如何同时管理,还包括两个容器如何通讯。这就引出来容器编排的概念,我们需要对容器进行一系列的调整,以满足我们的业务需要。

Docker Compose 需要额外安装,详情见https://docs.docker.com/compose/install/

Docker Compose是用于管理多容器的Docker应用。看一个示例:

docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 标明用docker compose的哪个版本,不同版本兼容的指令有区别
version: '3'
# 服务编排
services:
# 服务名称
nginx:
# 镜像地址
image: nginx
# 容器名称,不填则为“服务名_序号”
container_name: nginx
# 设置volume
volumes:
- nginx.conf:/etc/nginx/nginx.conf:ro
# 端口映射
ports:
- "8080:80"
api:
image: xxx.com/api:latest
container_name: api

nginx.conf

1
2
3
4
5
6
7
server{
listen 80;
server_name xxx.yyy.com;
location / {
proxy_pass http://api;
}
}

上面的服务编排就是对api容器做了一个nginx反向代理,可以看到使用docker-compose可以让容器间的通讯更加简单,直接用Container Name加Container内部端口即可。

使用docker-compose必须将文件命名成”docker-compose.yaml”,使用方法如下:

首先进入”docker-compose.yaml”文件所在目录

  • docker-compose up -d

    根据编排生成所需的容器并启动,-d表示后台执行

  • docker-compose start

    启动编排的容器

  • docker-compose stop

    停止编排的容器

  • docker-compose restart

    重启编排的容器

  • docker-compose pull

    强制拉取编排所需的镜像,因为在上例中,api使用的是latest镜像,当又有新的lastest镜像被push到Registry时,如果本地又存在一个旧的api:latest,那么docker compose 不会自动更新镜像,需要强制用”docker-compose pull”强制拉取,再执行”docker-compose up”

  • docker-compose build

    docker compose的容器镜像还可以直接用Dockerfile

    1
    2
    3
    4
    5
    version: '3'
    services:
    api:
    build: ./
    container_name: api

    表示编排容器时,需要先在本地用Dockerfile构建一个镜像,如果Dockerfile文件发生改变,而本地又有旧镜像,那么docker compose 同样不会自动更新镜像,需要执行”docker-compose build”强制构建,再执行”docker-compose up”

评论和共享

我的三十岁

发布在 life

孔老夫子说:“吾十有五,而志于学。三十而立,四十而不惑,五十而知天命,六十而耳顺,七十而从心所欲,不逾矩” 。然而我的而立之年,却是感受了两次“死亡”……

阅读全文

目录

  1. 一、K8S集群搭建
    1. 1.1 VPC组网
    2. 1.2 NAT网关与EIP打通网络
    3. 1.3 使用Kubeasz部署K8S集群
  2. 二、部署Gitlab实战
    1. 2.1 K8S Dashboard
    2. 2.2 PV与PVC
    3. 2.3 K8S部署Gitlab
    4. 2.4 使用Ingress-Nginx和阿里云SLB暴露服务
      1. 2.4.1 部署Ingress-Nginx
      2. 2.4.2 给gitlab配置ingress
      3. 2.4.3 设置阿里云SLB

前言:

考虑到公司持续集成与docker容器技术实施已有一段时间,取得了不错的效果,但对于设备运维、系统隔离、设备利用率和扩展性还有待提升,综合目前比较成熟的微服务技术,打算把现有业务迁移到K8S集群。

由于公司所有业务均部署在阿里云上,最开始就调研了阿里云自己提供的Kubernetes集群,但后来还是放弃了,主要考虑几方面:

  • 阿里云K8S集群尚不成熟,使用的版本也相对较老,不能及时更新版本
  • 阿里云K8S集群目前只支持多主多从结构,同时限定Master节点只能是3个,不能增减,这对于小型业务或者巨型业务均不适用
  • 自建原生K8S集群更有利于拓展和理解整体结构

接下来会详细介绍在阿里云搭建原生Kubernetes集群的过程。

一、K8S集群搭建

下面的实战操作基于阿里云的VPC网络,在4台ECS上搭建K8S单主多从集群,部署Gitlab,Gitlab的数据存储在阿里云NAS上,服务通过SLB暴露至外网

  • 阿里云VPC * 1
    • EIP * 2
    • NAT网关 * 1
    • 共享流量包 * 1
  • 阿里云ECS(无外网IP) * 4
  • 阿里云SLB * 4
  • 阿里云NAS * 1

1.1 VPC组网

对于VPC,新建交换机,目标网段用192.168.0.0/24,4台ECS的内网IP分别设置为192.168.0.1 ~ 192.168.0.4

1

1.2 NAT网关与EIP打通网络

由于VPC网络内,所有的ECS没有配置外网IP,所以这里要配置NAT网关和弹性IP来打通外网和VPC的通讯。

  • 开通一个NAT网关,并加入到VPC内

  • 开通两个EIP,一个用于DNAT(VPC访问外网),另一个用于SNAT(外网访问EIP)

  • 绑定EIP到NAT网关

    2

  • 配置DNAT(外网访问VPC)

    3

    • 我们有4台ECS,每台机器的22端口分别映射到EIP的不同端口上,如23301~23304,该端口用于SSH访问ECS
    • 同时映射192.168.0.1的6443端口到EIP上,如映射至23443端口,该端口用于访问K8S集群的API,见第二章内容
  • 配置SNAT(VPC访问外网)

    4

配置完成后,便可以使用绑定DNAT的EIP的映射端口通过SSH访问ECS

1.3 使用Kubeasz部署K8S集群

搭建K8S集群相对比较简单,使用kubeaszAllinOne部署即可

  • 修改hosts文件,根据实际环境配置master、node、etc的ip
  • 这里将192.168.0.1设置为master,使用单主多从的方式
  • 配置完成后重启所有ECS

二、部署Gitlab实战

2.1 K8S Dashboard

部署好集群后,我们可以使用DNAT的EIP,通过映射端口23443访问K8S API和Dashboard

https://EIP:Port/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy

  • 进入后会要求输入API的账号密码,与1.3章节hosts文件里配置的账号密码一致

  • 通过账号密码验证后可看到K8S Dashboard登录界面

    5

  • 令牌可在Master节点通过以下命令获取

    kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}')

2.2 PV与PVC

K8S中的PV和PVC的概念这里不再多提,引用官方的一段解释:

A PersistentVolume (PV) is a piece of storage in the cluster that has been provisioned by an administrator. It is a resource in the cluster just like a node is a cluster resource. PVs are volume plugins like Volumes, but have a lifecycle independent of any individual pod that uses the PV. This API object captures the details of the implementation of the storage, be that NFS, iSCSI, or a cloud-provider-specific storage system.

A PersistentVolumeClaim (PVC) is a request for storage by a user. It is similar to a pod. Pods consume node resources and PVCs consume PV resources. Pods can request specific levels of resources (CPU and Memory). Claims can request specific size and access modes (e.g., can be mounted once read/write or many times read-only).

Gitlab for Docker中,我们看到Volumes 有三个,如下表所示

Local location Container location Usage
/srv/gitlab/data /var/opt/gitlab For storing application data
/srv/gitlab/logs /var/log/gitlab For storing logs
/srv/gitlab/config /etc/gitlab For storing the GitLab configuration files

所以我们也需要给Gitlab for K8S分配3个PV和PVC,这里我们用到了阿里云NAS

  • 给NAS添加挂载点,选择VPC网络和VPC的交换机

    6

  • 查看挂载地址

    7

  • SSH登录Master节点,挂载NAS,并创建文件夹(注意PV的path必须已存在才可以成功建立,所以需要先在NAS中创建文件夹)

    1
    2
    3
    4
    5
    mkdir /nas
    sudo mount -t nfs -o vers=4.0 xxx.xxx.nas.aliyuncs.com:/ /nas
    mkdir -p /gitlab/data
    mkdir -p /gitlab/logs
    mkdir -p /gitlab/config
  • 编写PV和PVC的YAML,根据实际需求替换server节点的NAS挂载地址配置以及storage大小配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    apiVersion: v1
    kind: Namespace
    metadata:
    name: gitlab
    labels:
    name: gitlab
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
    name: gitlab-data
    labels:
    release: gitlab-data
    namespace: gitlab
    spec:
    capacity:
    storage: 500Gi
    accessModes:
    - ReadWriteMany
    persistentVolumeReclaimPolicy: Retain
    nfs:
    path: /gitlab/data
    server: xxx.xxx.nas.aliyuncs.com
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
    name: gitlab-config
    labels:
    release: gitlab-config
    namespace: gitlab
    spec:
    capacity:
    storage: 1Gi
    accessModes:
    - ReadWriteMany
    persistentVolumeReclaimPolicy: Retain
    nfs:
    path: /gitlab/config
    server: xxx.xxx.nas.aliyuncs.com
    ---
    apiVersion: v1
    kind: PersistentVolume
    metadata:
    name: gitlab-log
    labels:
    release: gitlab-log
    namespace: gitlab
    spec:
    capacity:
    storage: 1Gi
    accessModes:
    - ReadWriteMany
    persistentVolumeReclaimPolicy: Retain
    nfs:
    path: /gitlab/log
    server: xxx.xxx.nas.aliyuncs.com
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: gitlab-data-claim
    namespace: gitlab
    spec:
    accessModes:
    - ReadWriteMany
    resources:
    requests:
    storage: 500Gi
    selector:
    matchLabels:
    release: gitlab-data
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: gitlab-config-claim
    namespace: gitlab
    spec:
    accessModes:
    - ReadWriteMany
    resources:
    requests:
    storage: 1Gi
    selector:
    matchLabels:
    release: gitlab-config
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: gitlab-log-claim
    namespace: gitlab
    spec:
    accessModes:
    - ReadWriteMany
    resources:
    requests:
    storage: 1Gi
    selector:
    matchLabels:
    release: gitlab-log

2.3 K8S部署Gitlab

接下来补全Gitlab的Deployment和Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitlab
namespace: gitlab
spec:
selector:
matchLabels:
app: gitlab
replicas: 1
strategy:
type: Recreate
template:
metadata:
labels:
app: gitlab
spec:
containers:
- image: gitlab/gitlab-ce:latest
name: gitlab
ports:
- containerPort: 80
name: gitlab-http
- containerPort: 443
name: gitlab-https
- containerPort: 22
name: gitlab-ssh
volumeMounts:
- name: gitlab-config
mountPath: /etc/gitlab
- name: gitlab-log
mountPath: /var/log/gitlab
- name: gitlab-data
mountPath: /var/opt/gitlab
volumes:
- name: gitlab-data
persistentVolumeClaim:
claimName: gitlab-data-claim
- name: gitlab-config
persistentVolumeClaim:
claimName: gitlab-config-claim
- name: gitlab-log
persistentVolumeClaim:
claimName: gitlab-log-claim
---
kind: Service
apiVersion: v1
metadata:
name: gitlab-service
labels:
app: gitlab-service
namespace: gitlab
spec:
selector:
app: gitlab
ports:
- protocol: TCP
name: gitlab-https
port: 443
targetPort: 443
- protocol: TCP
name: gitlab-http
port: 80
targetPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: gitlab-ssh-service
labels:
app: gitlab-ssh-service
namespace: gitlab
spec:
type: NodePort
selector:
app: gitlab
ports:
- protocol: TCP
name: gitlab-ssh
port: 22
targetPort: 22
nodePort: 30000
  • 注意在Deployment中,开放了Gitlab Pod的80、443和22端口,用于Gitlab的HTTP、HTTPS和SSH的访问

  • 创建了2个Service,第一个只将80和443端口开放到Cluster IP上,第二个Service通过NodePort将22端口映射到NodeIp的30000端口上

  • 我们将2.2章节PV与PVC中的相关代码和上面的代码合并,并命名成gitlab.yaml,上传到Master节点,执行命令

    1
    kubectl apply -f gitlab.yaml
  • 接下来进入Gitlab的Pod,修改gitlab的域名,并启用https访问

    1
    2
    3
    4
    5
    6
    7
    8
    kubectl get pod --namespace=gitlab
    # 获得gitlab pod名称后
    kubectl exec -it gitlab-xxxx-xxxx --namespace=gitlab /bin/bash
    # 进入pod后
    vi /etc/gitlab/gitlab.rb
    # 修改external_url 'https://xxx.xxx.com',保存后退出
    gitlab-ctl reconfigure
    exit

到这里,配置与部署基本完成了,但我们还不能从外网访问Gitlab,不过至少可以在集群内验证配置是否正确。

  • 在Master节点查看Service

    1
    kubectl get svc --namespace=gitlab

    8

    可以看到443和80端口已经开发给Cluster IP,同时22端口映射到了30000的NodePort上

  • 通过curl命令查看访问结果

    1
    curl https://10.68.88.97 --insecure

    这时返回一串包含redirect的字符,如下

    <html><body>You are being <a href="https://10.68.88.97/users/sign_in">redirected</a>.</body></html>

    表示服务已部署成功

  • 如果有telnet客户端,还可以验证30000端口,在任何一个节点上执行任意一条命令

    1
    2
    3
    4
    telnet 192.168.0.1:30000
    telnet 192.168.0.2:30000
    telnet 192.168.0.3:30000
    telnet 192.168.0.4:30000

2.4 使用Ingress-Nginx和阿里云SLB暴露服务

K8S暴露服务的方法有3种:

  • ClusterIP:集群内可访问,但外部不可访问
  • NodePort:通过NodeIP:NodePort方式可以在集群内访问,结合EIP或者云服务VPC负载均衡也可在集群外访问,但开放NodePort一方面不安全,另一方面随着应用的增多不方便管理
  • LoadBalancer:某些云服务提供商会直接提供LoadBalancer模式,将服务对接到负载均衡,其原理是基于kubernetes的controller做二次开发,并集成到K8S集群,使得集群可以与云服务SDK交互

由于我们的集群搭建在阿里云上,所以第一时间想到的是LoadBalancer方案,但很遗憾,没办法使用,原因如下:

回归到NodePort的方式,目前已有的解决方案是基于Ingress的几款工具,如Ingress-Nginx、Traefik-Ingress,他们的对比如下(注意,目前的版本是IngressNginx 0.13.0、Traefik 1.6)

  • IngressNginx和Traefik都是通过hostname方式反向代理已解决端口暴露问题
  • IngressNginx依赖于Nginx,功能更多;Traefik不依赖Nginx,所以更轻量
  • IngressNginx支持4层和7层LB,但4层也不好用,Traefik只支持7层代理
  • 目前网上关于IngressNginx的文章都是beta 0.9.X版本的信息,而IngressNginx在Github的地址也变化了,直接由Kubernetes维护,所以网上的文章基本没参考性,还需看官方文档,但是官方文档极其混乱和不完善!!! 后面会有填坑指南。

最终我们还是选择了Ingress-Nginx,结合阿里云SLB,最终的拓扑图如下所示:

9

其原理是:

  • 通过Service的ClusterIP负载Pod
  • 通过Ingress-Nginx监听Ingress配置,动态生成Nginx,并将Nginx暴露到23456的NodePort
  • 通过阿里云SLB监听所有节点的23456端口

接下来看详细步骤。

2.4.1 部署Ingress-Nginx

主要参考https://kubernetes.github.io/ingress-nginx/deploy/,并做一些小调整

  • 替换gcr.io的镜像为阿里云镜像
  • 暴露服务端口到NodePort 23456
  • 整合成一个ingress-nginx.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
apiVersion: v1
kind: Namespace
metadata:
name: ingress-nginx
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: default-http-backend
labels:
app: default-http-backend
namespace: ingress-nginx
spec:
replicas: 1
selector:
matchLabels:
app: default-http-backend
template:
metadata:
labels:
app: default-http-backend
spec:
terminationGracePeriodSeconds: 60
containers:
- name: default-http-backend
# Any image is permissible as long as:
# 1. It serves a 404 page at /
# 2. It serves 200 on a /healthz endpoint
image: registry.cn-shenzhen.aliyuncs.com/heygears/defaultbackend:1.4
livenessProbe:
httpGet:
path: /healthz
port: 8080
scheme: HTTP
initialDelaySeconds: 30
timeoutSeconds: 5
ports:
- containerPort: 8080
resources:
limits:
cpu: 10m
memory: 20Mi
requests:
cpu: 10m
memory: 20Mi
---
apiVersion: v1
kind: Service
metadata:
name: default-http-backend
namespace: ingress-nginx
labels:
app: default-http-backend
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: default-http-backend
---
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-configuration
namespace: ingress-nginx
labels:
app: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: tcp-services
namespace: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
name: udp-services
namespace: ingress-nginx
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: nginx-ingress-clusterrole
rules:
- apiGroups:
- ""
resources:
- configmaps
- endpoints
- nodes
- pods
- secrets
verbs:
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- apiGroups:
- ""
resources:
- services
verbs:
- get
- list
- watch
- apiGroups:
- "extensions"
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- "extensions"
resources:
- ingresses/status
verbs:
- update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: nginx-ingress-role
namespace: ingress-nginx
rules:
- apiGroups:
- ""
resources:
- configmaps
- pods
- secrets
- namespaces
verbs:
- get
- apiGroups:
- ""
resources:
- configmaps
resourceNames:
# Defaults to "<election-id>-<ingress-class>"
# Here: "<ingress-controller-leader>-<nginx>"
# This has to be adapted if you change either parameter
# when launching the nginx-ingress-controller.
- "ingress-controller-leader-nginx"
verbs:
- get
- update
- apiGroups:
- ""
resources:
- configmaps
verbs:
- create
- apiGroups:
- ""
resources:
- endpoints
verbs:
- get
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: nginx-ingress-role-nisa-binding
namespace: ingress-nginx
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: nginx-ingress-role
subjects:
- kind: ServiceAccount
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: nginx-ingress-clusterrole-nisa-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: nginx-ingress-clusterrole
subjects:
- kind: ServiceAccount
name: nginx-ingress-serviceaccount
namespace: ingress-nginx
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: nginx-ingress-controller
namespace: ingress-nginx
spec:
replicas: 1
selector:
matchLabels:
app: ingress-nginx
template:
metadata:
labels:
app: ingress-nginx
annotations:
prometheus.io/port: '10254'
prometheus.io/scrape: 'true'
spec:
serviceAccountName: nginx-ingress-serviceaccount
containers:
- name: nginx-ingress-controller
image: registry.cn-shenzhen.aliyuncs.com/heygears/nginx-ingress-controller:0.13.0
args:
- /nginx-ingress-controller
- --default-backend-service=$(POD_NAMESPACE)/default-http-backend
- --configmap=$(POD_NAMESPACE)/nginx-configuration
- --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
- --udp-services-configmap=$(POD_NAMESPACE)/udp-services
- --annotations-prefix=nginx.ingress.kubernetes.io
env:
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
livenessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
readinessProbe:
failureThreshold: 3
httpGet:
path: /healthz
port: 10254
scheme: HTTP
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 1
---
kind: Service
apiVersion: v1
metadata:
name: ingress-nginx-service
namespace: ingress-nginx
spec:
selector:
app: ingress-nginx
ports:
- protocol: TCP
port: 80
# 从默认20000~40000之间选一个可用端口,让ingress-controller暴露给外部的访问
nodePort: 23456
type: NodePort

上传到Master节点后执行命令:

1
kubectl apply -f ingress-nginx.yaml

2.4.2 给gitlab配置ingress

修改2.3章节的gitlab.yaml,添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: gitlab-ingress
namespace: gitlab
annotations:
nginx.ingress.kubernetes.io/force-ssl-redirect: "true" # 强制http重定向到https
nginx.ingress.kubernetes.io/ssl-passthrough: "true" # 将请求时的ssl传递到此,如果后台监听80端口,则无需此配置
nginx.ingress.kubernetes.io/proxy-body-size: "0" # 设置client_max_body_size为0
spec:
rules:
- host: git.xxx.com # hostname
http:
paths:
- path: /
backend:
serviceName: gitlab-service
servicePort: 443 # 监听443端口

重新执行

1
kubectl apply -f gitlab.yaml

这里就有几个坑了:

  • 网上很多关于Ingress-Nginx的文章比较老旧,与新版annotations的配置不同,还是要以官方文档为准

  • annotations的配置与nginx实际配置的名称不同,不要错填,还是要以文档为准。比如上面例子中,nginx.ingress.kubernetes.io/proxy-body-size 实际上在nginx里是client_max_body_size,不要错填成nginx.ingress.kubernetes.io/client_max_body_size

  • 官方文档也有不清楚的地方,比如部分配置没有示例说明,还有的示例错误。比如上面例子中,官方给出的例子是

    10

    而事实上,value必须用双引号,否则配置将无效

2.4.3 设置阿里云SLB

阿里云SLB的设置比较简单

  • 后台服务器添加所有的K8S节点

    11

  • 配置监听,HTTP:80 -> 23456 ,HTTPS:443 -> 23456 并配置SSL证书, TCP:22 -> 30000,需要注意的是HTTP和HTTPS监听需要勾选SLB监听协议,以配合2.4.2章节中的force-ssl-redirect重定向,HTTPS中的SSL将配合ssl-passthrough传递到后台

    12

    13

  • 把SLB的公网IP添加到域名解析

至此,所有的配置完成。

评论和共享

RabbitMQ 基础介绍

发布在 architecture

目录

  1. 一、什么是RabbitMQ
  2. 二、 RabbitMQ简介
    1. 2.1 术语
    2. 2.2 基本特性
  3. 三、 官方Demo与思考
    1. 3.1 Hello World
      1. 3.1.1 操作
      2. 3.1.2 思考
    2. 3.2 工作队列
      1. 3.2.1 操作
      2. 3.2.2 思考
    3. 3.3 发布/订阅
      1. 3.3.1 操作
      2. 3.3.2 思考
    4. 3.4 路由
      1. 3.4.1 操作
      2. 3.4.2 思考
    5. 3.5 主题交换机
      1. 3.5.1 操作
      2. 3.5.2 思考
    6. 3.6 远程过程调用RPC
      1. 3.6.1 操作
      2. 3.6.2 思考
  4. 四、 思考总结
  5. 五、 场景模拟

一、什么是RabbitMQ

RabbitMQ是一个消息代理。

RabbitMQ基于AMQP协议用Erlang编写。

什么是AMQP?

  • AMQP(高级消息队列协议)是一个网络协议。它支持符合要求的客户端应用(application)和消息中间件代理(messaging middleware broker)之间进行通信。

简单来说: MQ是邮递过程,寄信人去邮局把信件放入邮箱,邮递员就会把信件投递到收件人

二、 RabbitMQ简介

2.1 术语

术语 类比
生产者Producing 寄信人,信件的来源
消息Message 信件
交换机Exchange 邮局,信件分发的场所
队列Queue 邮箱,存储邮件
绑定Binding 邮局信件和邮箱之间的关系,邮局信件按收件省市划分后归纳到不同邮箱
消费者Consuming 收件人,信件的归宿

1

2.2 基本特性

  • 消息只存储在Quene中

    • 只有邮箱,也就是Queue具有存储功能,Exchange不能存储消息
  • RoutingKey

    • RoutingKey就是消息的收件地址
    • RoutingKey可以直接写成XXX格式,也可以写成XXX.XXX.XXX格式,也可以为空
  • 交换机有4种类型

    • Direct 直连交换机
    • Fanout 扇形交换机
  • Topic 主题交换机
  • Headers 头交换机

三、 官方Demo与思考

3.1 Hello World

2

3.1.1 操作

生产者:

1
2
3
4
5
6
7
8
- 连接MQ
- 申明Queue
MQ.Queue.Name = "hello"
- 发送信息
MQ.Publish
Exchange = ""
RoutingKey = "hello"
Body = "hello wolrd"

消费者:

1
2
3
4
5
6
7
- 连接MQ
- 申明Queue
MQ.Queue.Name = "hello"
- 消费信息
MQ.Consume
Queue.Name= "hello"
Auto ack = true //注意自动ack

3.1.2 思考

  • 为何没有给Exchange命名?Exchange是什么类型?
  • 为何在生产者和消费者都申明了Queue?
  • RoutingKey和Queue.Name 有什么关系?

3.2 工作队列

3

3.2.1 操作

生产者:

1
2
3
4
5
6
7
8
9
10
- 连接MQ
- 申明Queue
MQ.Queue.Name = "task_queue"
MQ.Queue.Durable = true //注意Queue持久化
- 发送信息
MQ.Publish
Exchange = ""
RoutingKey = "task_queue"
Body = "hello wolrd"
delivery_mode = persistent or 2 //注意消息持久化

多个消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
- 连接MQ
- 申明Queue
MQ.Queue.Name = "task_queue"
MQ.Queue.Durable = true
- QoS
MQ.Qos
prefetch_count = 1 //注意QoS
- 消费信息
MQ.Consume
Queue.Name= "task_queue"
Auto ack = false //注意手动ack
- Sleep模拟处理事务
- 手动ack

3.2.2 思考

  • 多个消费者在Direct交换机,相同Queue下,如何接收消息?
  • 如何让Queue和消息持久化?
  • 可以直接给3.1中的hello队列赋予持久优设置吗?
  • 持久化有什么用?
  • 默认交换机是持久化的吗?
  • 手动ack和自动ack的区别?
  • 忘记ack怎么办?
  • QoS(Quality of Service)如何实现?

3.3 发布/订阅

4

3.3.1 操作

生产者:

1
2
3
4
5
6
7
8
9
10
- 连接MQ
- 申明Exchange
MQ.Exchange.Name = "logs"
MQ.Exchange.Type = "fanout"
MQ.Exchange.Durable = true
- 发送信息
MQ.Publish
Exchange = "logs"
RoutingKey = "" //注意RoutingKey
Body = "hello wolrd"

多个消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 连接MQ
- 申明Exchange
MQ.Exchange.Name = "logs"
MQ.Exchange.Type = "fanout"
MQ.Exchange.Durable = true
- 申明临时Queue
MQ.Queue.exclusive= true //注意临时Queue
- 绑定
Binding //注意绑定
Exchange = "logs"
Queue.Name = MQ.Queue.Name
RoutingKey = ""
- 消费信息
MQ.Consume
Queue.Name = MQ.Queue.Name
Auto ack = false
- Sleep模拟处理事务
- 手动ack

3.3.2 思考

  • 扇形交换机的RoutingKey是如何配置的?
  • 扇形交换机的主要用途?
  • 什么是临时队列?
  • 临时队列与持久化可以同时设置吗?
  • 扇形交换机可以在生产者设置持久化的队列吗?

3.4 路由

5

6

3.4.1 操作

生产者:

1
2
3
4
5
6
7
8
9
10
- 连接MQ
- 申明Exchange
MQ.Exchange.Name = "direct_logs"
MQ.Exchange.Type = "direct" //注意Exchange类型
MQ.Exchange.Durable = true
- 发送信息
MQ.Publish
Exchange = "direct_logs"
RoutingKey = "info" or "error" or "warn" //注意RoutingKey
Body = "hello wolrd"

多个消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 连接MQ
- 申明Exchange
MQ.Exchange.Name = "direct_logs"
MQ.Exchange.Type = "direct"
MQ.Exchange.Durable = true
- 申明临时Queue
MQ.Queue.exclusive= true
- 绑定
Binding
Exchange = "direct_logs"
Queue.Name = MQ.Queue.Name
RoutingKey = "info" or "error" or "warn"
- 消费信息
MQ.Consume
Queue.Name = MQ.Queue.Name
Auto ack = false
- Sleep模拟处理事务
- 手动ack

3.4.2 思考

  • 直连交换机多重绑定和扇形交换机有什么区别?
  • 直连交换机RoutingKey绑定有什么限制?

3.5 主题交换机

7

3.5.1 操作

生产者:

1
2
3
4
5
6
7
8
9
10
- 连接MQ
- 申明Exchange
MQ.Exchange.Name = "topic_logs"
MQ.Exchange.Type = "topic"
MQ.Exchange.Durable = true
- 发送信息
MQ.Publish
Exchange = "topic_logs"
RoutingKey = "this.abc" or "that.abc.xyz" or "abc" //注意RoutingKey写法
Body = "hello wolrd"

多个消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 连接MQ
- 申明Exchange
MQ.Exchange.Name = "topic_logs"
MQ.Exchange.Type = "topic"
MQ.Exchange.Durable = true
- 申明临时Queue
MQ.Queue.exclusive= true
- 绑定
Binding
Exchange = "topic_logs"
Queue.Name = MQ.Queue.Name
RoutingKey = "*.abc" or "#.abc" or "*.abc.*" //注意binding写法
- 消费信息
MQ.Consume
Queue.Name = MQ.Queue.Name
Auto ack = false
- Sleep模拟处理事务
- 手动ack

3.5.2 思考

  • *# 的区别?
  • 绑定键为 * 的队列会取到一个路由键为空的消息吗?
  • a.*.#a.#的区别在哪儿?

3.6 远程过程调用RPC

8

3.6.1 操作

调用端(生产者):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 调用本机某方法
调用另一个服务的接口
- 连接MQ
- 申明临时Queue
MQ.Queue.exclusive= true //注意临时Queue,用于接收回传信息
- 消费信息
MQ.Consume
Queue.Name= MQ.Queue.Name
Auto ack = true
- 发送信息
MQ.Publish
Exchange = ""
RoutingKey = "rpc_queue" //注意发送信息的队列,是服务端的队列,不是上面的临时队列
CorrelationId: corrId, //唯一id,用以与回调结果匹配
ReplyTo: q.Name, //回传地址,用以服务端将结果返回
Body = "123456" //传输id
- 接收结果
匹配CorrelationId 和 Server.CorrelationId
获得信息

服务端(消费者):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 连接MQ
- 申明Queue
MQ.Queue.Name= "rpc_queue" //注意此队列和调用者的临时队列不同
- QoS
MQ.Qos
prefetch_count = 1 //注意QoS
- 消费信息
MQ.Consume
Queue.Name= MQ.Queue.Name
Auto ack = false
- 调用接口
查询id相关信息
- 发送信息
MQ.Publish
Exchange = ""
RoutingKey = Client.ReplyTo
CorrelationId = Client.CorrelationId
Body = id相关信息

3.6.2 思考

  • 上面例子中使用了临时队列和非持久化队列,会出现什么问题?
  • 如果服务器发生故障,并且抛出异常,应该被转发到客户端吗?
  • 当没有服务器运行时,客户端如何作出反映?

四、 思考总结

  • Q:为何没有给Exchange命名?Exchange是什么类型?

    • 没有命名的交换机是默认(匿名)交换机
    • 默认交换机是Direct类型
  • Q:为何在生产者和消费者都申明了Queue?

    • 声明交换机和队列只能生效一次
    • 由于生产者和消费者无法确定启动顺序,所以两处申明可以防止招不到交换机或队列造成消息丢失
  • Q:RoutingKey和Queue.Name 有什么关系?

    • 默认或者匿名交换机的消息将会根据指定的routing_key分发到指定的队列

  • Q:多个消费者在Direct交换机,相同Queue下,如何接收消息?

    • 轮询分发
  • Q:如何让Queue和消息持久化?

    • 对于交换机和队列,设置其Durable属性
    • 对于消息,设置其发送模式为Persistent(部分语言用编码2表示)
  • Q:可以直接给3.1中的hello队列赋予持久优设置吗?

    • 不可以,队列只可申明一次,改动属性会报异常
  • Q:持久化有什么用?

    • 持久化会使MQ服务退出后,以申明的交换机、队列不仍存在,队列内的数据仍存在
    • 但持久化不能保证所有的数据都不会丢失
  • Q:默认交换机是持久化的吗?

  • Q:手动ack和自动ack的区别?

    • 自动ack会在消费者收到消息时就自动发送确认
    • 手动ack需要消费者自己手动发送确认
    • 消息没有超时的概念
    • 当消息被RabbitMQ发送给消费者之后,马上就会在内存中移除
    • 自动ack场景下,当消费者执行一个费时任务时,MQ崩溃,会导致消息丢失
    • 手动ack场景下,当消费者执行一个费时任务时,MQ崩溃,消息会被重新推送
  • Q:忘记ack怎么办?

    • 忘记ack会让MQ不断重复发送信息,导致MQ内存增加
    • 可以在MQ中查询messages_unacknowledged字段,手动处理
  • Q:QoS(Quality of Service)如何实现?

    • 设置MQ的QoS相关配置
    • 设置prefetch_count = 1

  • Q:扇形交换机的RoutingKey是如何配置的?

    • 填写为空
  • Q:扇形交换机的主要用途?

    • 发送广播
  • Q:什么是临时队列?

    • 临时队列随机生成队列名称
    • 当与消费者断开连接的时候,这个队列应当被立即删除
  • Q:临时队列与持久化可以同时设置吗?

    • 不可以,消费者断开后随机队列就删除,所以一旦MQ退出,消费者就与MQ断开连接,随机队列就会删除,不能设置持久化
  • Q:扇形交换机可以在生产者设置持久化的队列吗?

    • 可以,以保证所有消息不丢失。

    • 但是要注意,一般扇形交换机每个消费者一般独占一条队列,如果多个消费者共用一条队列,会跟直连交换机一样进行轮询分发

    • 如果在生产者设置10个持久化队列,但有20个消费者,每个消费者独占一条队列,那么只有其中的10个可以获取全部信息,其他10个消费者无法使用

    • 如果在生产者设置10个持久化队列,但有20个消费者,每2个消费者共用一条队列,那么20个消费者将轮询消费信息,不能收到完整的全部的信息

    • 所以扇形交换机一般不按照上面的方式使用

  • Q:直连交换机多重绑定和扇形交换机有什么区别?

    • 扇形交换机不能通过RoutingKey过滤消息
  • Q:直连交换机RoutingKey多重绑定有什么限制?

    • 不能用通配符方式模糊过滤消息

  • Q: *# 的区别?

    • (星号) 用来表示一个单词
    • (井号) 用来表示任意数量(零个或多个)单词
  • Q:绑定键为 * 的队列会取到一个路由键为空的消息吗?

    • 不能,星号表示至少一个单词
  • Q:a.*.#a.#的区别在哪儿?

    • 前者不可以匹配 a
    • 后者可以匹配 a

  • Q:上面例子中使用了临时队列和非持久化队列,会出现什么问题?
    • 没启动服务端时,客户机消息丢失
    • 客户端发送给服务端后,客户端断开连接,会导致回传信息丢失
  • Q:如果服务器发生故障,并且抛出异常,应该被转发到客户端吗?
    • 一般来说不需要
  • Q:当没有服务器运行时,客户端如何作出反映?
    • 对于不重要的信息,采用临时队列,丢失消息即可
    • 对于重要信息,采用持久化队列,等待服务器处理,并根据实际情况定时处理(清除或消息补偿)

五、 场景模拟

  • 账号注册后一系列短信邮件通知
    • 使用扇形交换机,短信和邮件服务作为消费者申明临时队列
    • 使用Auto ack
  • 秒杀下单
    • 使用直连交换机,秒杀服务作为生产者,订单中心作为消费者,都需要申明持久化队列
    • 使用手动ack
  • 下单后支付并返回结果
    • 使用直连交换机,下单服务作为生产者,支付服务作为消费者,都需要申明持久化队列
    • 使用手动ack
    • 消息安全性要求很高,需要消息补偿机制
  • 用户下单(订单服务接收下单请求,异步调用仓库服务查询库存是否足够)
    • 使用RPC
    • 仓库服务申明异步调用队列 A
    • 订单服务申明回传队列 B
    • 订单服务将待查询的产品id和B的地址发送到A
    • 仓库服务从A中消费信息,处理订单服务请求,并把结果发送到B
    • 订单服务从B中消费信息,接收仓库服务的查询结果

评论和共享

目录

  1. 前言
  2. 一、安装Docker
    1. 1.1 关闭selinux
    2. 1.2 删除CentOS自带Docker
    3. 1.3 安装Docker CE
    4. 1.4 安装Docker Compose
  3. 二、安装NextCloud
    1. 2.1 使用docker-compose安装NextCloud
    2. 2.2 设置开机启动
  4. 三、Nginx反向代理
    1. 3.1 域名解析
    2. 3.2 申请CA证书
    3. 3.3 安装Nginx
    4. 3.4 配置Nginx
    5. 3.5 开放防火墙端口
    6. 3.6 端口转发
  5. 四、DDNS
    1. 4.1 创建阿里云AccessKey
    2. 4.2 安装
    3. 4.3 配置

前言

本文涉及的操作系统、软件平台和其他环境如下:

  • 云盘服务器
    • CentOS 7
    • WD红盘/阵列盒(可选)
    • 所在网络需要有公网IP
    • Docker CE
    • Docker Compose
    • Nginx with SSL
  • 阿里云
    • DNS
    • CA证书

一、安装Docker

1.1 关闭selinux

如果没有专业的运维,建议关闭selinux,以免后续配置引起冲突

修改” /etc/selinux/config “文件,设置SELINUX=disabled ,保存并重启服务器

1.2 删除CentOS自带Docker

CentOS 7自带了旧版本Docker,所以先删除,如果服务器已经安装Docker请谨慎执行!!!

1
sudo yum -y remove docker docker-common container-selinux

1.3 安装Docker CE

Docker分CE和EE两个版本,这里我们用开源免费的CE版即可

1
2
3
4
5
6
7
8
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce
# 启动docker
sudo systemctl start docker
# 查看版本确认是否安装成功
docker --version

1.4 安装Docker Compose

Docker Compose是用来管理和配置多个Docker的工具,后面我们会用到它来部署NextCloud

1
2
3
4
sudo curl -L https://github.com/docker/compose/releases/download/1.18.0/docker-compose-`uname -s`-`uname -m` -o /usr/bin/docker-compose
sudo chmod +x /usr/bin/docker-compose
# 查看版本确认是否安装成功
docker-compose --version

二、安装NextCloud

2.1 使用docker-compose安装NextCloud

这里使用的安装方式来自https://hub.docker.com/r/wonderfall/nextcloud/

有兴趣可以了解所有的配置和相关逻辑,下面仅列出使用方法:

  • 将存储盘或者本地磁盘的某个分区挂载到目录” /data “下
  • 在” /etc/nextcloud “目录下创建文件” docker-compose.yml “

    1
    2
    cd /etc/nextcloud
    vi docker-compose.yml

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    version: '2'
    services:
    nextcloud:
    image: wonderfall/nextcloud
    links:
    - nextcloud-db
    - redis
    environment:
    - UID=1000
    - GID=1000
    - UPLOAD_MAX_SIZE=30G #单个文件上传限制
    - APC_SHM_SIZE=128M
    - OPCACHE_MEM_SIZE=128
    - CRON_PERIOD=15m
    - TZ=Asia/Shanghai #修改时区
    - ADMIN_USER=admin
    - ADMIN_PASSWORD=admin
    - DOMAIN=xxxx #需要设置的域名
    - DB_TYPE=mysql
    - DB_NAME=nextcloud
    - DB_USER=nextcloud
    - DB_PASSWORD=xxxx #数据库nextcloud用户密码
    - DB_HOST=nextcloud-db
    volumes:
    - /data/docker/nextcloud/data:/data # /data/docker/nextcloud/XX 是挂载卷的位置
    - /data/docker/nextcloud/config:/config
    - /data/docker/nextcloud/apps:/apps2
    - /data/docker/nextcloud/themes:/nextcloud/themes
    expose:
    - 8888
    nextcloud-db:
    image: mariadb:10
    volumes:
    - /data/docker/nextcloud/db:/var/lib/mysql
    environment:
    - MYSQL_ROOT_PASSWORD=xxxx #数据库root用户密码
    - MYSQL_DATABASE=nextcloud
    - MYSQL_USER=nextcloud
    - MYSQL_PASSWORD=xxxx #数据库nextcloud用户密码
    redis:
    image: redis:alpine
    container_name: redis
    volumes:
    - /data/docker/nextcloud/redis:/data

    上面的配置中使用了mysql作为数据库,redis作为缓存,加速同步效率,将容器的持久数据挂载到/data目录,没有使用默认启用的全文检索工具solr,没有将端口映射至宿主机。至于如何通过宿主机访问nextcloud,请参考第三章。

  • 执行docker-compose命令部署docker容器

    1
    2
    cd /etc/nextcloud
    docker-compose up -d
  • 查看是否部署成功

    1
    docker ps -a

    1

    如果三个容器的STATUS都是UP,证明容器启动成功

  • 查看nextcloud容器ip

    1
    docker inspect root_nextcloud_1

    2

    如上图所示,查询并记录nextcloud容器的ip,供第三章使用

2.2 设置开机启动

  • 编辑 /etc/rc.local

    1
    vi /etc/rc.local
    1
    2
    3
    service docker start
    cd /etc/nextcloud
    docker-compose start
  • 赋予可执行权限

    1
    chmod +x /etc/rc.d/rc.local

三、Nginx反向代理

3.1 域名解析

将第二章docker-compose.yml 配置中的域名解析到服务器公网IP

3

3.2 申请CA证书

有条件可以购买收费CA证书,这里使用了阿里云的免费证书,现在(2017.12)阿里云刻意“隐藏”了免费证书的位置,按以下操作可以找到:

  • 保护类型选择“1个域名”
  • 选择品牌先选择赛门铁克Symantec
  • 这时候才能看到证书类型出现“免费型DV SSL”

4

购买证书后需要进行验证,通过后才能下载使用

5

下载证书请选择for Nginx,解压将其中两个文件(key和pem)拷贝至服务器” /etc/cert “目录

6

3.3 安装Nginx

如果已经安装Nginx可以跳过该步骤,否则可以执行下面的命令安装nginx

1
2
3
4
5
6
7
8
# 设置rpm源
sudo rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarch.rpm
# 安装nginx
sudo yum install -y nginx
# 开机自启动nginx
systemctl enable nginx
# 启动nginx
systemctl start nginx

3.4 配置Nginx

1
vi /etc/nginx/nginx.conf

修改配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
proxy_buffering off; #关闭代理缓存
server {
listen 23456 ssl ; #监听端口为23456,并启用ssl
server_name pan.xxx.com; #域名
ssl_certificate /etc/cert/xxxxx.pem; #pem文件路径
ssl_certificate_key /etc/cert/xxxxx.key; #key文件路径
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://172.18.0.4:8888; #填写nextcloud容器的ip,端口为8888
client_max_body_size 30000m; #允许用户上传文件的大小修改成30G
}
}
}

重启nginx

1
systemctl restart nginx

3.5 开放防火墙端口

执行命令:

1
2
firewall-cmd --zone=public --add-port=23456/tcp --permanent
firewall-cmd --reload

打开23456端口以免访问受限。

3.6 端口转发

最后,我们还需要把路由器的23456端口转发至NextCloud服务器(如192.168.2.254),路由器不同,配置方式也不同,下图是H3路由器配置方式:

10

到这里,我们就可以通过 https://pan.XXX.com:23456 访问NextCloud了,需要注意我们只配置了Nginx监听SSL,也就是输入URL时,不要忘记是https

需要提醒的是,如果服务器网络是动态IP,还需做DDNS,否则IP更换后,将不能通过域名访问NextCloud

四、DDNS

由于我们想用公司自己的二级域名,又有服务器,还有阿里云的SDK,所以我们没有使用花生壳等第三方解决方案,这里使用DDNS的方式来自https://github.com/rfancn/aliyun-ddns-client

4.1 创建阿里云AccessKey

  • 进入阿里云访问控制页面,新建domain用户,并自动生成AccessKey,确认后记录access_key和access_id,注意保密

    7

  • 分配域名管理权限给domain用户

    8

4.2 安装

  • 安装python的requests包

    1
    2
    3
    yum -y install epel-release
    yum install python-pip
    pip install requests
  • 下载源码到 /usr/local

    1
    2
    cd /usr/local
    wget https://github.com/rfancn/aliyun-ddns-client/archive/master.zip
  • 解压源码

    1
    2
    yum install -y unzip
    unzip master.zip

4.3 配置

  • 重命名ddns.conf.example

    1
    2
    cd /usr/local/aliyun-ddns-client-master
    mv ddns.conf.example ddns.conf
  • 编辑ddns.service

    1
    vi ddns.service

    修改WorkingDirectory

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [Unit]
    Description=Aliyun DDNS Client.
    Wants=network-online.target
    After=network.target network-online.target
    [Service]
    Type=simple
    WorkingDirectory=/usr/local/aliyun-ddns-client-master #修改工作目录
    ExecStart=/usr/bin/python ddns.py
  • 复制服务文件

    1
    2
    cp ddns.timer /usr/lib/systemd/system
    cp ddns.service /usr/lib/systemd/system
  • 修改ddns.conf

    1
    vi ddns.conf
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    [DEFAULT]
    # 填写阿里云domain用户的access_id
    access_id=XXXX
    # 填写阿里云domain用户的access_key
    access_key=XXXXX
    # Optional: not used now
    interval=600
    # Optional: turn on debug mode or not
    debug=true
    [DomainRecord1]
    # 填写一级域名,如google.com
    domain=xxxx.com
    # 填写子域名,如pan,注意不要写成pan.xxxx.com
    sub_domain=pan
    # Required: resolve type, now it only supports 'A'
    type=A
  • 启动服务并验证配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    cd /usr/local/aliyun-ddns-client-master
    python ddns.py
    # 如果显示 2017-12-26 15:28:15 [INFO] Successfully updated DomainRecord[pan.xxxx.com]则环境正常
    systemctl daemon-reload
    systemctl start ddns.timer
    # 开机自启动
    systemctl enable ddns.timer
    # 查看服务状态
    systemctl status ddns.timer -l

    如果如下图所示,则服务正常:

    9

最后的最后,我们可以在内网或者公网通过 https://pan.xxx.com:23456 访问NextCloud,初始管理员账号密码为admin,登陆后就可以配置和使用了。

评论和共享

Scrum实施总结(一)

发布在 agile

目录

  1. 一、团队简介
    1. 1.1 团队组成
    2. 1.2 使用工具
  2. 二、 实践总结:sprint 1 - sprint 4
    1. 2.1 Sprint 1
      1. 2.1.1 迭代周期统计
      2. 2.1.2 优点
      3. 2.1.3 问题
    2. 2.2 Sprint 2
      1. 2.2.1 迭代周期统计
      2. 2.2.2 优点
      3. 2.2.3 问题
    3. 2.3 Spring 3
      1. 2.3.1 迭代周期统计
      2. 2.3.2 优点
      3. 2.3.3 成员评价和总结
    4. 2.4 Sprint 4
      1. 2.4.1 迭代周期统计
      2. 2.4.2 优点
      3. 2.4.3 问题

一、团队简介

1.1 团队组成

Dev Team:

  • 后台开发 * 1
  • 前端开发 * 1
  • 客户端开发 * 1
  • 测试 * 1

PO:

  • PO * 1
  • UI设计 * 1

Scrum Master:

  • scrum master * 1

1.2 使用工具

  • Leangoo
  • Jenkins
  • Selenium
  • TestStack.White

二、 实践总结:sprint 1 - sprint 4

2.1 Sprint 1

整个项目团队是新组建的,没有实施敏捷的经验

2.1.1 迭代周期统计

Sprint长度:2 week

统计:

WEB 总计
功能点 42 42
工作量 61 61
bug 未做探索性测试
完成功能点 39 39

结果:

功能没有完成,迭代失败

2.1.2 优点

  • 团队对于敏捷的兴趣比较高,工作积极性高

2.1.3 问题

  • 问题暴露不及时:由于团队刚刚组建,成员彼此不熟悉,遇到问题不习惯当面沟通

    在Sprint3开始后(一个月),该情况明显好转,遇到问题时,开发人员逐渐习惯整个团队来讨论解决问题

  • 没有做探索性测试:sprint1中,只针对AC做自动化测试,忽略了探索性测试

    Sprint2开始后,补充探索性测试

  • 发布时间长:没有为开发、测试、生产环境做独立配置,容易出错

    Sprint2开始后,上jenkins持续集成,Sprint3开始后,整体正常

  • 开发团队不习惯任务领取

    由于Sprint1没有持续集成,发布麻烦,所以开发不愿意逐个领任务。要求Sprint2开始后,开发人员要按优先级逐个领取任务并签名

  • 演示太慢:花费2个小时

    Sprint2开始后,由测试人员来演示,由于测试人员对AC和流程比较熟悉,演示速度能控制到1小时左右

  • 不会看燃尽图:PO和开发不会看燃尽图,不会评估开发进度

    Sprint2教会团队和PO通过Leangoo查看燃尽图

  • 立会内容表达不准确:开发团队每日立会讲述的内容不准确

    制定了一个参考模板:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    功能
    今天在XX配合下做完了XX相关的X个功能的开发/测试,工作量共计X,是否达成目标
    今天还在做XX相关的X个功能,工作量共计X
    明天会做完XX相关的X个功能,工作量为X
    存在什么问题,没打成目标的原因
    任务
    今天做完了哪些任务,正在做哪些任务
    明天要做完的任务,任务预期是什么时间完成
    存在什么问题

2.2 Sprint 2

2.2.1 迭代周期统计

Sprint长度:2 week

统计:

WEB 总计
功能点 39 39
工作量 64 64
bug 4 4
完成功能点 20 20

结果:

功能没有完成,迭代失败

2.2.2 优点

  • 团队沟通和配合更默契
  • 上了Jenkins,发布更快捷

2.2.3 问题

  • 团队对全量回归测试认识不足:验收会前2个小时,团队修复2个bug,之后没有及时做全量回归测试,导致验收时19个功能没通过验收

    在Sprint3开始后,将全量回归测试加入jenkins持续集成中

  • 开发人员不按需求擅自增改功能

    要求开发人员遇到有争议的需求,必须和PO商量解决,不能擅自增改AC

  • 大任务拆解:对于网站页面UI这种比较大的任务,不方便评估工作量,也不方便及时测试

    工作量过大的任务,根据模块、页面拆解成子任务

  • 测试环境预览:PO和UI设计也想看到测试环境,好尽早判断开发是否复合需求

    给PO权限,让PO和设计人员也参与到测试中

  • 优先级调整和需求替换:PO刚开始不太能把控需求的优先级,Sprint开始前,如何确认和调整?PO不清楚如果有需求变化,需要改变或增加功能,需要怎么做?

    • 在下一个Sprint开始前,PO先大概罗列近期要做的功能,开发先写出AC并评估出工作量,PO根据工作量再考虑下一个Sprint要做的功能,从而重排PB优先级。在下一个Spring计划会上,确定该周期要做的功能。
    • 一般不建议在迭代周期内增改需求,如果迭代开始后,一定有需求需要修改或增加,一般有两个方式:等量替换和请客吃饭。从现在的周期内拿出等量或工作量略大于需要修改增加的功能,保证迭代能按期完成。或PO请团队吃饭,让团队加班完成。但不论哪一种,都不能经常使用。

2.3 Spring 3

2.3.1 迭代周期统计

Sprint长度:2 week

统计:

WEB CLIENT 总计
功能点 35 36 71
工作量 67 70 137
bug 13 2 15
完成功能点 35 36 71

结果:

迭代成功

2.3.2 优点

  • 迭代周期中替换了部分需求,没有影响到迭代周期
  • 自动化测试也集成到jenkins中,整套流程正规化

2.3.3 成员评价和总结

  • 开发评价Sprint1是忙,很多技术债要偿还,要适应新的开发模式;Sprint2是茫,有点不知所措,团队和协作不流畅;Sprint3团队已经逐渐熟悉这种模式
  • PO已经找到和开发团队沟通的方法,PO遇到疑问时,如果不是很重要的,会在每天下午5点后空闲时间和开发沟通,不打断开发人员正常的节奏

2.4 Sprint 4

2.4.1 迭代周期统计

Sprint长度:1 week

统计:

WEB CLIENT 总计
功能点 12 27 39
工作量 35 17 52
bug 0 9 9
完成功能点 13 27 40

结果:

迭代成功

2.4.2 优点

  • 从Spring4开始,尝试将迭代周期长度变成1周,以适应产品发布的实际情况,过度良好
  • PO和设计人员也“参与”到测试中

2.4.3 问题

  • Scrum的节奏比较快,团队压力比较大,容易疲劳

    在Spring4后,整个团队休息一周,调整节奏、偿还技术债

评论和共享

目录

  1. 一、使用插件
    1. 1.1 安装插件
    2. 1.2 启用插件
  2. 二、配置权限
    1. 2.1 角色管理
    2. 2.2 用户管理
    3. 2.3 分配角色
  3. 三、权限问题

jenkins默认的权限管理不支持用户分组或者按项目划分权限,所以如果团队有这种需求,需要安装插件。下面将介绍使用插件来实现用户角色的管理。

一、使用插件

1.1 安装插件

安装Role-based Authorization Strategy插件

1

1.2 启用插件

进入“系统管理”的“Configure Global Security”界面,配置如下:

2

  • 启动安全
  • 使用Jenkins专有用户数据库
  • 取消勾选“允许用户注册”,一般由管理员分配
  • 使用“Role-Based Strategy”策略

【注意】:使用Role-Based Strategy策略后,先不要注销管理员账号,否则会由于后续角色权限没配置而无法登陆,如果遇到这种问题,参考第三章

二、配置权限

正确安装了插件后,就能在“系统管理”中看到“Manage and Assign Roles”

3

2.1 角色管理

  • 进入“Manage Roles”

    4

  • 根据需要编辑Global roles

    5

    • 这里创建两个全局角色
    • admin有所有权限
    • project用于分配给项目组,这里只开放只读权限
  • 根据需要编辑Project roles

    6

    • 这里通过编写表达式让角色拥有对应项目的权限
    • 如果要匹配前缀是“Dent”的项目,表达式为“Dent.*”,和一般通配符表达式不同的是,星号前面还有一个点,不要忘记了

需要注意Project roles和Global roles配置的project不同,在2.3章节中会进一步解释。

2.2 用户管理

创建用户的步骤非常简单,如下所示:

  • 进入“系统管理”的“管理用户”

    7

  • 左边栏“新建用户”,按内容填写

    8

2.3 分配角色

编辑好角色和用户后,现在把它们关联起来,让权限生效:

  • 同样进入“系统管理”的“Manage and Assign Roles”,点击“Assign Roles”

    9

  • 参考下面的配置

    10

    • 在Global roles中,添加所有的用户,然后分配对应的全局角色
    • 在Item roles中,也要添加所有的用户(管理员用户可以不用分配),分配对应的项目角色

    这里解释一下,所有的用户都分配了两个角色,Global roles和Item roles,很容易不理解或者容易犯错的是只给一般用户分配Item roles,也就是项目角色,这样分配后,用户登陆会提示没有Overall的Read权限,也就是说用户虽然有某个项目的权限,他可以通过某个项目的URL去访问,但没有总体预览权限,没法进入首页。所以必须给用户配置全局角色,以获得Overall权限。

三、权限问题

在配置权限时,如果因为配置不当,导致管理员账号不能登陆jenkins,可以按下面的方式操作:

  • 编辑config.xml

    • 你可以在宿主机的映射位置如“/var/lib/docker/volumes/jenkins/_data/”找到该文件
    • 或者在docker容器内的“/var/jenkins_home”找到
  • 修改useSecurity为false

    1
    <useSecurity>false</useSecurity>
  • 删除authorizationStrategy、securityRealm节点

  • 重启jenkins for docker

    1
    2
    docker stop myjenkins
    docker start myjenkins

上面的操作可以清空权限系统,用管理员登录后重新配置即可。

jenkins四期介绍到此就结束了,然而jenkins在实际项目中的应用功能还远远不止如此,pipline,运维监控等高级玩法,jenkins+交通灯、报警器的搞怪玩法,以后有机会再整理分享出来。

评论和共享

作者的图片

Wu Rang

Everything begin with HelloWorld!


System Architect


Guangzhou