Подготовка
В этом разделе урока вы будете работать с оставшимися файлами шаблонов и немного более продвинутыми вещами, которые можно настроить в диаграмме kubernetes-dashboard
и которые мы еще не рассматривали. На этом мы завершим учебное преобразование объектов из “обычного” графика Helm в график на основе HULL.
Но прежде чем погрузиться в это, подготовьте рабочую папку и скопируйте результат предыдущих разделов:
cd ~/kubernetes-dashboard-hull && cp -R 06_service_api/ 07_advanced_configuration && cd 07_advanced_configuration/kubernetes-dashboard-hull
Подготовьте values.yaml
:
sed '/ objects:/Q' values.full.yaml > values.yaml
и еще раз проверьте результат:
cat values.yaml
в настоящее время выглядит следующим образом:
metrics-server:
enabled: false
hull:
config:
specific:
externalPort: 443
protocolHttp: false
rbac:
clusterReadOnlyRole: false
clusterRoleMetrics: true
settings: {}
pinnedCRDs: {}
Для напоминания исходного шаблона проверьте папку templates еще раз:
ls ../kubernetes-dashboard/templates
показывая следующие файлы:
NOTES.txt clusterrole-metrics.yaml clusterrolebinding-readonly.yaml ingress.yaml psp.yaml secret.yaml servicemonitor.yaml
_helpers.tpl clusterrole-readonly.yaml configmap.yaml networkpolicy.yaml role.yaml service.yaml
_tplvalues.tpl clusterrolebinding-metrics.yaml deployment.yaml pdb.yaml rolebinding.yaml serviceaccount.yaml
Большинство из них уже рассмотрены, а вот оставшиеся и являются темой данного раздела:
networkpolicy.yaml
pdb.yaml
psp.yaml
servicemonitor.yaml
Такие аспекты, как NetworkPolicies, PodDisruptionBudgets, PodSecurityPolicies и ServiceMonitors можно считать более продвинутыми вазами использования, поскольку вы можете видеть, что для использования каждой из этих функций на диаграмме kubernetes-dashboard
необходимо явно включить их.
Использование встроенных ServiceMonitors и CRDs
Концепция ServiceMonitor является частью концепции Prometheus Operator и предлагает удобный способ добавления и удаления конечных точек, которые экспортируют метрики Prometheus, чтобы экземпляр Prometheus мог динамически отслеживать их.
Поскольку это очень популярная концепция экспортирования метрик приложений, ServiceMonitor имеет свой собственный тип объекта servicemonitor
в мире HULL.
По умолчанию ServiceMonitor находится в values.yaml
:
cat ../kubernetes-dashboard/values.yaml | grep "^serviceMonitor:" -B 1 -A 2
отключен:
serviceMonitor:
# Whether or not to create a Prometheus Operator service monitor.
enabled: false
Здесь вы можете создать объект servicemonitor
в соответствии с заложенной в него логикой:
echo ' objects:
servicemonitor:
dashboard:
enabled: false
endpoints: |-
_HT![
{{ if (index . "$").Values.hull.config.specific.protocolHttp }}
{ port: "http" }
{{ else }}
{ port: "https" }
{{ end }}
]
selector:
matchLabels: _HT&dashboard' >> values.yaml
Включите ServiceMonitor с помощью этого:
echo 'hull:
objects:
servicemonitor:
dashboard:
enabled: true' > ../configs/servicemonitor.yaml
&& helm template -f ../configs/disable-default-rbac.yaml -f ../configs/servicemonitor.yaml .
и протестируйте вывод:
---
# Source: kubernetes-dashboard/templates/hull.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
annotations: {}
labels:
app.kubernetes.io/component: dashboard
app.kubernetes.io/instance: release-name
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: kubernetes-dashboard
app.kubernetes.io/part-of: undefined
app.kubernetes.io/version: 2.5.0
helm.sh/chart: kubernetes-dashboard-5.2.0
name: release-name-kubernetes-dashboard-dashboard
spec:
endpoints:
- port: https
selector:
matchLabels:
app.kubernetes.io/component: dashboard
app.kubernetes.io/instance: release-name
app.kubernetes.io/name: kubernetes-dashboard
Если есть какие-либо другие CustomResourceDefinitions или CustomResources для создания в вашей диаграмме Helm, это также возможно в HULL благодаря:
- возможность развертывания ваших CRD с помощью папки
crds
- возможность указать любой CustomResource как экземпляр объекта
customresource
. Для CustomResources вам дополнительно нужно указатьkind
иapiVersion
помимо свободной формыspec
вашего объекта.
Указание сетевых политик
В networkpolicy.yaml
:
cat ../kubernetes-dashboard/templates/networkpolicy.yaml
является довольно легким:
# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{{ if .Values.networkPolicy.enabled -}}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ template "kubernetes-dashboard.fullname" . }}
labels:
app: {{ template "kubernetes-dashboard.name" . }}
chart: {{ template "kubernetes-dashboard.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
annotations:
{{- if .Values.commonAnnotations }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
podSelector:
matchLabels:
{{ include "kubernetes-dashboard.matchLabels" . | nindent 6 }}
ingress:
- ports:
{{- if .Values.protocolHttp }}
- port: http
protocol: TCP
{{- else }}
- port: https
protocol: TCP
{{- end -}}
{{- end -}}
и отключен по умолчанию, как показано на рисунке:
cat ../kubernetes-dashboard/values.yaml | grep "^networkPolicy:" -B 1 -A 3
в values.yaml
:
networkPolicy:
# Whether to create a network policy that allows/restricts access to the service
enabled: false
Добавление новых фиксированных меток здесь:
labels:
app: {{ template "kubernetes-dashboard.name" . }}
chart: {{ template "kubernetes-dashboard.chart" . }}
release: {{ .Release.Name }}
heritage: {{ .Release.Service }}
выглядит довольно несовместимым с обработкой метаданных других объектов на этой диаграмме и, вероятно, даже не имеет функциональности. В любом случае, для полноты картины добавьте их в метки метаданных новых объектов, перенеся основную логику.
Что касается podSelector
, вы можете использовать функцию HULL hull.tranformation.selector
для создания соответствующих меток, чтобы соответствовать dashboard
pods.
Вот и все, запишите это в values.yaml
:
echo ' networkpolicy:
dashboard:
enabled: false
labels:
app: _HT!{{- default (index . "$").Chart.Name (index . "$").Values.nameOverride | trunc 63 | trimSuffix "-" -}}
chart: _HT!{{- printf "%s-%s" (index . "$").Chart.Name (index . "$").Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
release: _HT!{{ (index . "$").Release.Name }}
heritage: _HT!{{ (index . "$").Release.Service }}
podSelector:
matchLabels: _HT&dashboard
ingress: |-
_HT![
{ ports:
[
{
protocol: TCP,
{{ if (index . "$").Values.hull.config.specific.protocolHttp }}
port: "http"
{{ else }}
port: "https"
{{ end }}
}
]
}
]'>> values.yaml
и примените команду helm template
на включенной сетевой политике:
echo 'hull:
objects:
networkpolicy:
dashboard:
enabled: true' > ../configs/networkpolicy.yaml
&& helm template -f ../configs/disable-default-rbac.yaml -f ../configs/networkpolicy.yaml .
и проверьте правильность результатов:
---
# Source: kubernetes-dashboard/templates/hull.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
annotations: {}
labels:
app: kubernetes-dashboard
app.kubernetes.io/component: dashboard
app.kubernetes.io/instance: release-name
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: kubernetes-dashboard
app.kubernetes.io/part-of: undefined
app.kubernetes.io/version: 2.5.0
chart: kubernetes-dashboard-5.2.0
helm.sh/chart: kubernetes-dashboard-5.2.0
heritage: Helm
release: release-name
name: release-name-kubernetes-dashboard-dashboard
spec:
ingress:
- ports:
- port: https
protocol: TCP
podSelector:
matchLabels:
app.kubernetes.io/component: dashboard
app.kubernetes.io/instance: release-name
app.kubernetes.io/name: kubernetes-dashboard
Указание бюджета PodDisruptionBudget
PodDisruptionBudgets имеет три поля:
- Селектор ярлыка .spec.selector для указания набора стручков, к которым он применяется. Это поле является обязательным.
- .spec.minAvailable – описание количества стручков из этого набора, которые должны быть доступны после выселения, даже при отсутствии выселенного стручка. minAvailable может быть как абсолютным числом, так и процентом.
- .spec.maxUnavailable (доступно в Kubernetes 1.7 и выше), который представляет собой описание количества стручков из этого набора, которые могут быть недоступны после выселения. Это может быть как абсолютное число, так и процент.
Посмотрите на наше определение PodDisruptionBudget:
cat ../kubernetes-dashboard/templates/pdb.yaml
# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{{ if .Values.podDisruptionBudget.enabled -}}
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
labels:
{{- include "kubernetes-dashboard.labels" . | nindent 4 }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
annotations:
{{- if .Values.commonAnnotations }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
name: {{ template "kubernetes-dashboard.fullname" . }}
spec:
{{- if .Values.podDisruptionBudget.minAvailable }}
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
{{- end }}
{{- if .Values.podDisruptionBudget.maxUnavailable }}
maxUnavailable: {{ .Values.podDisruptionBudget.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{ include "kubernetes-dashboard.matchLabels" . | nindent 6 }}
{{- end -}}
В файле values.yaml
:
cat ../kubernetes-dashboard/values.yaml | grep "^podDisruptionBudget:" -B 3 -A 6
значения по умолчанию опущены для minAvailable
и maxUnavailable
:
## podDisruptionBudget
## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/
podDisruptionBudget:
enabled: false
## Minimum available instances; ignored if there is no PodDisruptionBudget
minAvailable:
## Maximum unavailable instances; ignored if there is no PodDisruptionBudget
maxUnavailable:
Здесь вы снова можете использовать удобное преобразование селектора _HT&
и указать шаблон объекта:
echo ' poddisruptionbudget:
dashboard:
enabled: false
selector:
matchLabels: _HT&dashboard' >> values.yaml
и снова сделайте быструю проверку, включив PodDisruptionBudget:
echo 'hull:
objects:
poddisruptionbudget:
dashboard:
enabled: true' > ../configs/pdb.yaml
&& helm template -f ../configs/disable-default-rbac.yaml -f ../configs/pdb.yaml .
что довольно просто:
---
# Source: kubernetes-dashboard/templates/hull.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
annotations: {}
labels:
app.kubernetes.io/component: dashboard
app.kubernetes.io/instance: release-name
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: kubernetes-dashboard
app.kubernetes.io/part-of: undefined
app.kubernetes.io/version: 2.5.0
helm.sh/chart: kubernetes-dashboard-5.2.0
name: release-name-kubernetes-dashboard-dashboard
spec:
selector:
matchLabels:
app.kubernetes.io/component: dashboard
app.kubernetes.io/instance: release-name
app.kubernetes.io/name: kubernetes-dashboard
Настройка политики безопасности PodSecurityPolicies
Когда вы просмотрите оставшийся файл PodSecurityPolicy:
cat ../kubernetes-dashboard/templates/psp.yaml
вы обнаружите PodSecurityPolicy и – что удивительно – еще одну роль и RoleBinding:
# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
{{ if .Values.podSecurityPolicy.enabled -}}
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
name: {{ template "kubernetes-dashboard.fullname" . }}-psp
labels:
{{- include "kubernetes-dashboard.labels" . | nindent 4 }}
{{- if .Values.commonLabels }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }}
{{- end }}
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
{{- if .Values.commonAnnotations }}
{{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }}
{{- end }}
spec:
privileged: false
fsGroup:
rule: RunAsAny
runAsUser:
rule: RunAsAny
runAsGroup:
rule: RunAsAny
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
volumes:
- 'configMap'
- 'secret'
- 'emptyDir'
allowPrivilegeEscalation: false
hostNetwork: false
hostIPC: false
hostPID: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ template "kubernetes-dashboard.fullname" . }}-psp
labels:
{{ include "kubernetes-dashboard.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ template "kubernetes-dashboard.fullname" . }}-psp
subjects:
- kind: ServiceAccount
name: {{ template "kubernetes-dashboard.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ template "kubernetes-dashboard.fullname" . }}-psp
labels:
{{ include "kubernetes-dashboard.labels" . | nindent 4 }}
rules:
- apiGroups:
- extensions
- policy/v1beta1
resources:
- podsecuritypolicies
verbs:
- use
resourceNames:
- {{ template "kubernetes-dashboard.fullname" . }}-psp
{{- end -}}
values.yaml
cat ../kubernetes-dashboard/values.yaml | grep "^podSecurityPolicy:" -B 2 -A 3
предлагает следующее содержание podSecurityPolicy
:
## podSecurityPolicy for fine-grained authorization of pod creation and updates
podSecurityPolicy:
# Specifies whether a pod security policy should be created
enabled: false
Кроме удивления от неожиданных объектов здесь нет ничего нового, и мы можем прямо преобразовать их, добавив PodSecurityPolicy, Role и RoleBinding:
echo ' podsecuritypolicy:
dashboard:
enabled: false
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: "*"
privileged: false
fsGroup:
rule: RunAsAny
runAsUser:
rule: RunAsAny
runAsGroup:
rule: RunAsAny
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
volumes:
- configMap
- secret
- emptyDir
allowPrivilegeEscalation: false
hostNetwork: false
hostIPC: false
hostPID: false
rolebinding:
psp:
enabled: _HT?(index . "$").Values.hull.objects.podsecuritypolicy.dashboard.enabled
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: _HT^psp
subjects:
- kind: ServiceAccount
namespace: _HT!{{ (index . "$").Release.Namespace }}
name: _HT^default
role:
psp:
enabled: _HT?(index . "$").Values.hull.objects.podsecuritypolicy.dashboard.enabled
rules:
psp:
apiGroups:
- extensions
- policy/v1beta1
resources:
- podsecuritypolicies
verbs:
- use
resourceNames: _HT![ {{ include "hull.metadata.fullname" (dict "PARENT_CONTEXT" (index . "$") "COMPONENT" "psp") }} ]' >> values.yaml
Включите PSP с помощью этого:
echo 'hull:
objects:
podsecuritypolicy:
dashboard:
enabled: true' > ../configs/psp.yaml
&& helm template -f ../configs/disable-default-rbac.yaml -f ../configs/psp.yaml .
---
# Source: kubernetes-dashboard/templates/hull.yaml
apiVersion: policy/v1beta1
kind: PodSecurityPolicy
metadata:
annotations:
seccomp.security.alpha.kubernetes.io/allowedProfileNames: '*'
labels:
app.kubernetes.io/component: dashboard
app.kubernetes.io/instance: release-name
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: kubernetes-dashboard
app.kubernetes.io/part-of: undefined
app.kubernetes.io/version: 2.5.0
helm.sh/chart: kubernetes-dashboard-5.2.0
name: release-name-kubernetes-dashboard-dashboard
spec:
allowPrivilegeEscalation: false
fsGroup:
rule: RunAsAny
hostIPC: false
hostNetwork: false
hostPID: false
privileged: false
runAsGroup:
rule: RunAsAny
runAsUser:
rule: RunAsAny
seLinux:
rule: RunAsAny
supplementalGroups:
rule: RunAsAny
volumes:
- configMap
- secret
- emptyDir
---
# Source: kubernetes-dashboard/templates/hull.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
annotations: {}
labels:
app.kubernetes.io/component: psp
app.kubernetes.io/instance: release-name
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: kubernetes-dashboard
app.kubernetes.io/part-of: undefined
app.kubernetes.io/version: 2.5.0
helm.sh/chart: kubernetes-dashboard-5.2.0
name: release-name-kubernetes-dashboard-psp
rules:
- apiGroups:
- extensions
- policy/v1beta1
resourceNames:
- release-name-kubernetes-dashboard-psp
resources:
- podsecuritypolicies
verbs:
- use
---
# Source: kubernetes-dashboard/templates/hull.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
annotations: {}
labels:
app.kubernetes.io/component: psp
app.kubernetes.io/instance: release-name
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: kubernetes-dashboard
app.kubernetes.io/part-of: undefined
app.kubernetes.io/version: 2.5.0
helm.sh/chart: kubernetes-dashboard-5.2.0
name: release-name-kubernetes-dashboard-psp
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: release-name-kubernetes-dashboard-psp
subjects:
- kind: ServiceAccount
name: release-name-kubernetes-dashboard-default
namespace: default
Завершение работы
Преобразование объектов завершено, пришло время еще раз навести порядок.
-
Ваш
values.yaml
должен выглядеть следующим образом:metrics-server: enabled: false hull: config: specific: externalPort: 443 protocolHttp: false rbac: clusterReadOnlyRole: false clusterRoleMetrics: true settings: {} pinnedCRDs: {} objects: servicemonitor: dashboard: enabled: false endpoints: |- _HT![ {{ if (index . "$").Values.hull.config.specific.protocolHttp }} { port: "http" } {{ else }} { port: "https" } {{ end }} ] selector: matchLabels: _HT&dashboard networkpolicy: dashboard: enabled: false labels: app: _HT!{{- default (index . "$").Chart.Name (index . "$").Values.nameOverride | trunc 63 | trimSuffix "-" -}} chart: _HT!{{- printf "%s-%s" (index . "$").Chart.Name (index . "$").Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} release: _HT!{{ (index . "$").Release.Name }} heritage: _HT!{{ (index . "$").Release.Service }} podSelector: matchLabels: _HT&dashboard ingress: |- _HT![ { ports: [ { protocol: TCP, {{ if (index . "$").Values.hull.config.specific.protocolHttp }} port: "http" {{ else }} port: "https" {{ end }} } ] } ] poddisruptionbudget: dashboard: enabled: false selector: matchLabels: _HT&dashboard podsecuritypolicy: dashboard: enabled: false annotations: seccomp.security.alpha.kubernetes.io/allowedProfileNames: "*" privileged: false fsGroup: rule: RunAsAny runAsUser: rule: RunAsAny runAsGroup: rule: RunAsAny seLinux: rule: RunAsAny supplementalGroups: rule: RunAsAny volumes: - configMap - secret - emptyDir allowPrivilegeEscalation: false hostNetwork: false hostIPC: false hostPID: false rolebinding: psp: enabled: _HT?(index . "$").Values.hull.objects.podsecuritypolicy.dashboard.enabled roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: _HT^psp subjects: - kind: ServiceAccount namespace: _HT!{{ (index . "$").Release.Namespace }} name: _HT^default role: psp: enabled: _HT?(index . "$").Values.hull.objects.podsecuritypolicy.dashboard.enabled rules: psp: apiGroups: - extensions - policy/v1beta1 resources: - podsecuritypolicies verbs: - use resourceNames: _HT![ {{ include "hull.metadata.fullname" (dict "PARENT_CONTEXT" (index . "$") "COMPONENT" "psp") }} ]
-
Сделайте резервную копию того, что вы сделали в этой части руководства:
cp values.yaml values.tutorial-part.yaml
-
Поскольку сейчас существует блок
role
в текущемvalues.full.yaml
и нашем локальномvalues.yaml
, необходимо объединить их соответствующим образом. В противном случае второй ключrole
будет перезаписывать первый, что не очень полезно. Для создания окончательногоvalues.full.yaml
вам, следовательно, нужно сделать немного больше работы, но вы можете просто скопировать и вставить следующую “одну строчку”, и это должно сработать:sed '1,/objects:/d' values.full.yaml > _tmp && sed '/^ssssrole:/r'<( echo " psp:" echo " enabled: _HT?(index . "$").Values.hull.objects.podsecuritypolicy.dashboard.enabled" echo " rules:" echo " psp:" echo " apiGroups:" echo " - extensions" echo " - policy/v1beta1" echo " resources:" echo " - podsecuritypolicies" echo " verbs:" echo " - use" echo ' resourceNames: _HT![ {{ include "hull.metadata.fullname" (dict "PARENT_CONTEXT" (index . "$") "COMPONENT" "psp") }} ]' ) -i -- _tmp && cp values.yaml values.full.yaml && sed '/^ssssrole:/,$d' -i values.full.yaml && cat _tmp >> values.full.yaml && rm _tmp
Еще раз спасибо, в этой серии уроков осталось только подвести итоги.