404频道

学习笔记

阿里巴巴的财年为每年4月1日至次年3月31日,而4月1日至9月30日为财年的上半年。大部分的团队每半个财年要完成一次述职,而眼下的 9 月 30 日前又到了述职的日子。

在我过往的工作经历中,并没有述职的习惯,绩效的考核也相对随意一些,绩效的好坏跟主管的主管判断关系比较大,但通常绩效的好坏跟个人的成绩也是成正比的,不会出现较大的偏差。

在阿里绩效的绩效考核会非常被重视,希望能够通过公平的方法来分配高低绩效,降低主管的主观判断,当然主观的判断仍然是无法避免的,而且也会仍然占较大的比重。

述职可以作为一种常见的职场总结方式,也是绩效考核的一个重要的参考依据。

记得刚来阿里的时候,团队处于业务的爆发期,述职并不是太被重视,尤其是形式方面。某一年的述职中,我仅准备了可怜的三页 PPT,仅完成了做的事情的简单罗列,缺少一些自己的思考总结,当时就被喷的比较惨。当然述职中没有体现,并不代表平常就没有思考,即使平时没有思考也不代表就会影响实际的工作。

一到述职,自己也曾经非常头疼,经过了这几年的洗礼,自己对职场的述职也有了一点点自己的总结,希望能帮助到大家。

为什么要做述职?

存在即合理,我认为的述职有这么几个好处:

  1. 对自己过去做的事情的非常好的总结,帮助自己重新认识自己。这也是最关键的一点。大家肯定有这样的体会,如果让你回想过去几个月甚至一年做的事情,恐怕很难一下子想全。在现代职场中,大家都是匆匆忙忙的赶路人,任务一个接一个的完成,很难像在学校读书一样有温故而知新的机会。而述职恰恰是一个非常好的机会,回顾过去一段时间内自己做的事情,找找自己的闪光点,反思一下如果一件事情再重新做,会不会做的更好更高效。人总是在不断的总结经验中成长,述职是个很不错的职场成长机会。
  2. 让自己了解别人做的事情。别人做的最好的事情、吃的亏、踩过的坑都已经帮你总结好了,这是多好的一个学习机会。也许在吸取别人经验的过程中,会迸发出一些新的灵感。
  3. 让别人了解自己做的事情。自己做的申请同样需要传播给其他的同学,或许别人才能发现自己的闪光点,是个提升自己影响力的好时机(当然前提是自己要有货)。自己身上的不足也能够让别人发现,进而别人来帮助自己成长。
  4. 作为主管绩效考核的依据。这一点我倒认为并没有那么关键,自己做的事情自己的能力在日常工作中已经足够体现,不需要在述职的时候才做出判断,最多也就是作为判断的依据而已。
  5. 提升自己的演讲能力。演讲是一种能力,需要刻意练习。述职中要面对不同的人群,在有效的时间内将自己的事情给别人讲明白属实不是一件容易的事情。
  6. 通过述职这种形式化的方式来间接的促进员工在日常中工作的积极性。对于一部分员工,如果缺少了述职这种形式,在日常工作中就少了一份压力,工作中可能就会稍显懈怠。

述职的常见误区有哪些?

述职也并非百害而无一利,同样是一把双刃剑,如果能利用的好,会对自己有较大的帮助。如果利用不好,就容易反噬。

  1. 不要搞的花里胡哨太抽象。也是述职中最常见的问题,也是最让人诟病的。我见到过不少搞得花里胡哨的述职材料,一件很简单的事情非要说的很复杂,让人感觉很高大上,殊不知包装后,能听懂的人群更加少了。尽量少用 PPT,直接使用文档形式就非常不错,省去了大量排版的工作。图片也尽可能的简介,不要为了美观而消耗太多的时间。简简单单的真实就是最好的述职。
  2. 不要报喜不报忧。这也是个非常常见的问题。这种虚假的汇报,或许可以玩转一次述职,下次述职就很难遮盖过去了。或许一部分人可以糊弄过去,另外一部分人就未必。一个谎言的背后就得需要无数个谎言来弥补,实事求是在述职中非常的关键,严禁弄虚作假。
  3. 不要为了述职而述职。很多人都比较头疼述职,为了述职不得不做准备。其实这种心态就不太对,为了做而做,那么一定做不好。
  4. 不要消耗过多的时间来准备。如果一次要准备上好几天的时间就有点不值了,毕竟一年真正有效的工作时间才能有多少天呢,述职本身并不会产生业务价值,不要本末倒置。
  5. 不要陷在自己的主观世界中。要考虑到参加述职的人群,给别人讲明白自己做的事情。

述职的形式是什么样子的?

职场中的很多事情都有固定的套路,尤其是在一家公司内部。最好的方式就是参考老员工的述职报告,找到固定的套路,或者按照主管要求的套路来。最好不要试图找到一条新的方法,前辈们总结的路基本都不会错。

述职的内容基本要遵循 STAR 法则(即背景 Situation、任务 Task、行为 Action、结果 Result),主要包含几部分内容:

  1. Situation:事情的背景。
  2. Task:面临的挑战和遇到的困难。项目干系人,自己在其中承担的角色。
  3. Action:自己的解决方案。
  4. Result:获得的业务结果。能量化的一些业务结果最好要量化,这样会非常直观。

其中也可以讲讲自己的总结思考和个人成长等内容。

另外在讲述的时候要符合金字塔原理,核心的原则:

  1. 结论先行。
  2. 每层结论下面的论据不要超过7个。
  3. 每一个论点要言之有物,有明确的思想。

述职的材料该如何准备?

这里有几个建议:

  1. 如果有写周报的习惯,看看周报的内容。
  2. 看看各种群的聊天记录的内容。
  3. 看看自己的代码提交记录。
  4. 看看自己的文档。

当然,上面的一些前提都是平常要有一些积累,如果平常积累的多,那么述职的材料准备起来就比较简单快速。

最后,述职是一种管理员工的手段,作为员工在无法取消述职这种形式的时候,还是要尽可能将述职作为一种促进自己提升的手段,不要为了述职而述职。

容器中的执行topfree等命令展示出来的CPU,内存等信息是从/proc目录中的相关文件里读取出来的。而容器并没有对/proc/sys等文件系统做隔离,因此容器中读取出来的CPU和内存的信息是宿主机的信息,与容器实际分配和限制的资源量不同。

1
2
3
4
5
6
/proc/cpuinfo
/proc/diskstats
/proc/meminfo
/proc/stat
/proc/swaps
/proc/uptime

lxcfs是一个常驻进程运行在宿主机上,从而来自动维护宿主机cgroup中容器的真实资源信息与容器内/proc下文件的映射关系。

lxcfs实现的基本原理是通过文件挂载的方式,把cgroup中容器相关的信息读取出来,存储到lxcfs相关的目录下,并将相关目录映射到容器内的/proc目录下,从而使得容器内执行top,free等命令时拿到的/proc下的数据是真实的cgroup分配给容器的CPU和内存数据。

image.png

类别 容器内目录 宿主机lxcfs目录
cpu /proc/cpuinfo /var/lib/lxcfs/proc/cpuinfo
内存 /proc/meminfo /var/lib/lxcfs/proc/meminfo
/proc/diskstats /var/lib/lxcfs/proc/diskstats
/proc/stat /var/lib/lxcfs/proc/stat
/proc/swaps /var/lib/lxcfs/proc/swaps
/proc/uptime /var/lib/lxcfs/proc/uptime
/proc/loadavg /var/lib/lxcfs/proc/loadavg
/sys/devices/system/cpu/online /var/lib/lxcfs/sys/devices/system/cpu/online

在每个容器内仅需要挂载 lxcfs 在宿主机上的目录到容器中的目录即可。

多年前,我曾经阅读过两本当时非常流行的书籍《人性的弱点》和《人性的优点》,作者均为卡耐基。
image.png

而前段时间参加过一场[卡内基的培训](卡内基训练 (carnegiechina.com)),自己就有点傻傻的分不清楚此“卡”是否为彼“卡”。
image.png
于是搜索了一下资料,发现两“卡”均为 戴尔·卡耐基(Dale Carnegie,1888年11月24日—1955年11月1日),美国的作家和演讲者。只是因为翻译的偏差,导致出现了卡内基和卡耐基两种称呼。
image.png

1912 年创办了卡内基训练(Dale Carnegie Training)教导人们人际沟通及如何处理压力。
1936 年完成书籍《人性的弱点》又名《如何赢取友谊与影响他人》(_How to Win Friends and Influence People_)一书。

之所以会出现混淆,还有另外的名字也是跟卡耐基相关的:

  1. 安德鲁·卡内基(Andrew Carnegie,1835年11月25日—1919年8月11日):美国著名的钢铁大王,其名称要远大于戴尔·卡耐基
  2. 卡内基·梅隆大学(Carnegie Mellon University):由钢铁大王安德鲁·卡内基创办,全球大学排名上在前60名以内。

本文为对《GPT 图解 - 大模型是怎样构建的》一书的学习笔记,所有的例子和代码均来源于本书。

基本介绍

N-Gram 模型为语言模型的雏形。

基本思想为:一个词出现的概率仅依赖其前面的 N-1 个词。即通过有限的 N-1 个词来预测第 N 个词。

以 “我爱吃肉” 举例,分词为 [”我“, “爱”, “吃”, “肉”]。

  • 当 N=1 时,对应的序列为[”我“, “爱”, “吃”, “肉”],又成为 Unigram。
  • 当 N=2 时,对应的序列为[”我爱“, “爱吃”, “吃肉”],又成为 Bigram。
  • 当 N=3 时,对应的序列为[”我爱吃“, “爱吃肉”],又成为 Trigram。

2-Gram 的构建过程

将语料拆分为分词

将给定的语料库,拆分为以一个的分词。在真实的场景中需要使用分词函数,这里简单起见,使用了一个汉字一个分词的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
corpus = [ "我喜欢吃苹果",  
"我喜欢吃香蕉",
"她喜欢吃葡萄",
"他不喜欢吃香蕉",
"他喜欢吃苹果",
"她喜欢吃草莓"]

# 定义一个分词函数,将文本转换为单个字符的列表
def tokenize(text):
return [char for char in text] # 将文本拆分为字符列表
# 对每个文本进行分词,并打印出对应的单字列表
print("单字列表:")
for text in corpus:
tokens = tokenize(text)
print(tokens)

得到如下的结果:

1
2
3
4
5
6
7
单字列表:
['我', '喜', '欢', '吃', '苹', '果']
['我', '喜', '欢', '吃', '香', '蕉']
['她', '喜', '欢', '吃', '葡', '萄']
['他', '不', '喜', '欢', '吃', '香', '蕉']
['他', '喜', '欢', '吃', '苹', '果']
['她', '喜', '欢', '吃', '草', '莓']

计算每个 2-Gram(BiGram) 在语料库中的词频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 定义计算 N-Gram 词频的函数  
from collections import defaultdict, Counter # 导入所需库
def count_ngrams(corpus, n):
ngrams_count = defaultdict(Counter) # 创建一个字典,存储 N-Gram 计数
for text in corpus: # 遍历语料库中的每个文本
tokens = tokenize(text) # 对文本进行分词
for i in range(len(tokens) - n + 1): # 遍历分词结果,生成 N-Gram ngram = tuple(tokens[i:i+n]) # 创建一个 N-Gram 元组
prefix = ngram[:-1] # 获取 N-Gram 的前缀
token = ngram[-1] # 获取 N-Gram 的目标单字
ngrams_count[prefix][token] += 1 # 更新 N-Gram 计数
return ngrams_count
bigram_counts = count_ngrams(corpus, 2) # 计算 bigram 词频
print("bigram 词频:") # 打印 bigram 词频
for prefix, counts in bigram_counts.items():
print("{}: {}".format("".join(prefix), dict(counts)))

计算获取到如下的结果:

1
2
3
4
5
6
7
8
9
10
11
12
bigram 词频:
我: {'喜': 2}
喜: {'欢': 6}
欢: {'吃': 6}
吃: {'苹': 2, '香': 2, '葡': 1, '草': 1}
苹: {'果': 2}
香: {'蕉': 2}
她: {'喜': 2}
葡: {'萄': 1}
他: {'不': 1, '喜': 1}
不: {'喜': 1}
草: {'莓': 1}

即当第一个词为 ”我“,第二个词为”喜“在整个语料库中出现了 2 次。

如果为 3-Gram(TriGram),此时输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
bigram 词频:
我喜: {'欢': 2}
喜欢: {'吃': 6}
欢吃: {'苹': 2, '香': 2, '葡': 1, '草': 1}
吃苹: {'果': 2}
吃香: {'蕉': 2}
她喜: {'欢': 2}
吃葡: {'萄': 1}
他不: {'喜': 1}
不喜: {'欢': 1}
他喜: {'欢': 1}
吃草: {'莓': 1}

如果为 1-Gram(UniGram),此时输出结果如下:

1
2
bigram 词频:
: {'我': 2, '喜': 6, '欢': 6, '吃': 6, '苹': 2, '果': 2, '香': 2, '蕉': 2, '她': 2, '葡': 1, '萄': 1, '他': 2, '不': 1, '草': 1, '莓': 1}

可以看到已经退化为了每个单词在整个语料库中出现的次数。

计算每个 2-Gram 出现的概率

即给定前一个词,计算下一个词出现的概率。

1
2
3
4
5
6
7
8
9
10
11
# 定义计算 N-Gram 出现概率的函数  
def ngram_probabilities(ngram_counts):
ngram_probs = defaultdict(Counter) # 创建一个字典,存储 N-Gram 出现的概率
for prefix, tokens_count in ngram_counts.items(): # 遍历 N-Gram 前缀
total_count = sum(tokens_count.values()) # 计算当前前缀的 N-Gram 计数
for token, count in tokens_count.items(): # 遍历每个前缀的 N-Gram ngram_probs[prefix][token] = count / total_count # 计算每个 N-Gram 出现的概率
return ngram_probs
bigram_probs = ngram_probabilities(bigram_counts) # 计算 bigram 出现的概率
print("\nbigram 出现的概率 :") # 打印 bigram 概率
for prefix, probs in bigram_probs.items():
print("{}: {}".format("".join(prefix), dict(probs)))

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
bigram 出现的概率 :
我: {'喜': 1.0}
喜: {'欢': 1.0}
欢: {'吃': 1.0}
吃: {'苹': 0.3333333333333333, '香': 0.3333333333333333, '葡': 0.16666666666666666, '草': 0.16666666666666666}
苹: {'果': 1.0}
香: {'蕉': 1.0}
她: {'喜': 1.0}
葡: {'萄': 1.0}
他: {'不': 0.5, '喜': 0.5}
不: {'喜': 1.0}
草: {'莓': 1.0}

给定一个前缀,输出连续的文本

根据前面学习的语料信息,给定一个前缀,即可生成对应的文本内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 定义生成下一个词的函数  
def generate_next_token(prefix, ngram_probs):
if not prefix in ngram_probs: # 如果前缀不在 N-Gram 中,返回 None return None
next_token_probs = ngram_probs[prefix] # 获取当前前缀的下一个词的概率
next_token = max(next_token_probs,
key=next_token_probs.get) # 选择概率最大的词作为下一个词
return next_token

# 定义生成连续文本的函数
def generate_text(prefix, ngram_probs, n, length=6):
tokens = list(prefix) # 将前缀转换为字符列表
for _ in range(length - len(prefix)): # 根据指定长度生成文本
# 获取当前前缀的下一个词
next_token = generate_next_token(tuple(tokens[-(n-1):]), ngram_probs)
if not next_token: # 如果下一个词为 None,跳出循环
break
tokens.append(next_token) # 将下一个词添加到生成的文本中
return "".join(tokens) # 将字符列表连接成字符串

# 输入一个前缀,生成文本
generated_text = generate_text("我", bigram_probs, 2)
print("\n 生成的文本:", generated_text) # 打印生成的文本

给定了文本”我“,可以生成出如下结果:

1
生成的文本: 我喜欢吃苹果

总结

N-Gram 为非常简单的语言模型,可以根据给定的词来生成句子。

缺点:无法捕捉到距离较远的词之间的关系。

在字节的 AI 发布平台扣子中提供了创建机器人的功能,并且可以直接对接微信公众号,使用在微信公众号中回复消息,由大模型直接回复的效果。

操作步骤

整体操作步骤非常简单,需要申请一个 Coze 的账号和开通微信公众号的开发者功能。

微信公众号的开发者功能在这里配置:
image.png

点击创建 Bot 按钮,输入 Bot 名称后点击确认
image.png|570

在机器人设置页面,可以配置模型、选择自己训练的知识库等操作。
image.png

设置完成后,点击发布,选择微信公众号(订阅号)配置功能,设置对应的微信公众号的 AppID。
image.png

体验

访问公众号直接输入内容,可以看到自动回复内容。
IMG_3856.PNG.JPG

无意间发现了开源项目 mi-gpt,该项目可以将家里的小爱音箱接入到 GPT 中,增强小爱音箱的功能。在跟小爱音箱对话的过程中,可以根据特定的提示词走 GPT 来回答,而不是用小爱音箱原生的回复。

必备条件:

  1. 必须有一个小米音箱。
  2. 必须要有可以长期运行的服务器,可以是树莓派等设备。
  3. 要有一个 OpenAI 的账号,也可以用兼容 ChatGPT API 的国内大模型。

部署

部署比较简单,下面为我的部署过程,供大家参考。更详细的信息大家可以直接参考 github 项目中的相关文档

创建配置文件 .migpt.js

参考项目中的文件 .migpt.example.js内容:

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
// 小爱音箱扮演角色的简介
const botProfile = `
性别:女
性格:乖巧可爱
爱好:喜欢搞怪,爱吃醋。
`;

// 小爱音箱主人(你)的简介
const masterProfile = `
性别:男
性格:善良正直
其他:总是舍己为人,是傻妞的主人。
`;

export default {
bot: {
name: "傻妞",
profile: botProfile,
},
master: {
name: "陆小千",
profile: masterProfile,
},
speaker: {
// 小米 ID
userId: "12345", // 注意:不是手机号或邮箱,请在「个人信息」-「小米 ID」查看
// 账号密码
password: "xxx",
// 小爱音箱 ID 或在米家中设置的名称
did: "Redmi小爱触屏音箱8",
// 当消息以下面的关键词开头时,会调用 AI 来回复消息
callAIKeywords: ["请", "你", "傻妞"],
// 当消息以下面的关键词开头时,会进入 AI 唤醒状态
wakeUpKeywords: ["打开", "进入", "召唤"],
// 当消息以下面的关键词开头时,会退出 AI 唤醒状态
exitKeywords: ["关闭", "退出", "再见"],

// 进入 AI 模式的欢迎语
onEnterAI: ["你好,我是傻妞,很高兴认识你"],

// 退出 AI 模式的提示语
onExitAI: ["傻妞已退出"],

// AI 开始回答时的提示语
onAIAsking: ["让我先想想", "请稍等"],

// AI 结束回答时的提示语
onAIReplied: ["我说完了", "还有其他问题吗"],

// AI 回答异常时的提示语
onAIError: ["啊哦,出错了,请稍后再试吧!"],

// 无响应一段时间后,多久自动退出唤醒模式(默认 30 秒)
exitKeepAliveAfter: 30,

// TTS 指令,请到 https://home.miot-spec.com 查询具体指令
ttsCommand: [3, 1],

// 设备唤醒指令,请到 https://home.miot-spec.com 查询具体指令
wakeUpCommand: [3, 2],

// 是否启用流式响应,部分小爱音箱型号不支持查询播放状态,此时需要关闭流式响应
streamResponse: false,

// 查询是否在播放中指令,请到 https://home.miot-spec.com 查询具体指令
playingCommand: [2, 1, 1], // 默认无需配置此参数,播放出现问题时再尝试开启
},
};

必须要修改的内容涉及到如下字段,其他字段可以根据含义来定义:

  1. speaker.userId:小米的用户 ID。
  2. speaker.password:小米的账号密码。
  3. speaker.did:小爱音箱的 ID 或者小爱音箱在米家的设备名字。
  4. ttsCommand:需要设置。如果设置不正常,会导致小爱音箱无法播放 GPT 回复内容的情况。
  5. wakeUpCommand:需要设置。
  6. streamResponse:在某些音箱设备上需要关闭。我的设备因为无法读完完整的句子,选择了关闭该功能,相关参考:小爱音箱没有读完整个句子,总是戛然而止

ttsCommand 和 wakeUpCommand 需要在 https://home.miot-spec.com 页面搜索对应的音箱型号
image.png

点击规格后任选一个,选择 Intelligent Speaker,其中的 [3, 1] 对应的为 ttsCommand,[3, 2] 对应的为 wakeUpCommand。
image.png

创建配置文件 .env

该文件中需要配置 OPENAI 的账号信息,我这里直接采用了阿里云的通义千问大模型服务,API 是完全兼容的

参考文档《开通DashScope并创建API-KEY》 阿里云上开通大模型服务,获取到 API-KEY
image.png

参考项目中的文件.env.example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# OpenAI(也支持通义千问、MoonShot、DeepSeek 等模型)
OPENAI_MODEL=qwen-turbo
OPENAI_API_KEY=获取到的API_KEY
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1

# Azure OpenAI Service(可选)
# OPENAI_API_VERSION=2024-04-01-preview
# AZURE_OPENAI_API_KEY=你的密钥
# AZURE_OPENAI_ENDPOINT=https://你的资源名.openai.azure.com
# AZURE_OPENAI_DEPLOYMENT=你的模型部署名,比如:gpt-35-turbo-instruct

# 提示音效(可选,一般不用填,你也可以换上自己的提示音链接试试看效果)
# AUDIO_SILENT=静音音频链接,示例:https://example.com/slient.wav
# AUDIO_BEEP=默认提示音链接,同上
# AUDIO_ACTIVE=唤醒提示音链接,同上
# AUDIO_ERROR=出错了提示音链接,同上

# Doubao TTS(可选,用于调用第三方 TTS 服务,比如:豆包)
# TTS_DOUBAO=豆包 TTS 接口
# SPEAKERS_DOUBAO=豆包 TTS 音色列表接口

主要修改如下两个值:

  1. OPENAI_API_KEY:即为上文获取到阿里云模型服务灵积的 API-KEY。
  2. OPENAI_MODEL:支持的模型,可以在《支持的模型列表》中查询模型列表。

启动服务

提供了两种方式:docker 和 宿主机 Node.js 方式运行,我自然会选择更加简洁的 docker 方式。

执行 docker run -d --name mi-gpt --env-file $(pwd)/.env -v $(pwd)/.migpt.js:/app/.migpt.js idootop/mi-gpt:3.1.0 即可本地运行。

功能演示

提问小爱同学:“请问一下太阳的重量是多少”,小爱同学可以顺利的回答出答案。

通过 docker logs mi-gpt -f 可以看到如下的输出日志:

1
2
3
4
5
6
7
2024/06/12 16:15:38 Speaker 🔥 请问一下太阳的重量是多少

2024/06/12 16:15:38 Speaker 🔊 让我先想想

2024/06/12 16:15:41 Open AI ✅ Answer: 傻妞: 哦,太阳的重量可大了,它是个恒星,比我们的地球重得多。科学家们用的是质量而不是重量来衡量,太阳的质量大约是地球的333,000倍,真是个超级大块头,想想如果它能变成棉花糖,那得多软多亮啊!不过,太阳对我们来说太遥远了,它的重量咱们还是别去抱了,哈哈。

2024/06/12 16:15:41 Speaker 🔊 傻妞: 哦,太阳的重量可大了,它是个恒星,比我们的地球重得多。科学家们用的是质量而不是重量来衡量,太阳的质量大约是地球的333,000倍,真是个超级大块头,想想如果它能变成棉花糖,那得多软多亮啊!不过,太阳对我们来说太遥远了,它的重量咱们还是别去抱了,哈哈

在 k8s 中包含两类用户:

  1. ServiceAccount。又称服务账号,在运行 pod 时必须绑定 ServiceAccount,如果没有指定,则使用当前 namespace 下的 ServiceAccount default。是针对程序而言,用于 pod 中的程序访问 kube-apiserver。
  2. 普通用户。在 k8s 中并没有使用单独的对象来存储,而是通过了分发证书、外部用户认证系统等方式实现,是针对用户而言。

1. X509 证书认证

使用场景:使用 kubectl 访问 k8s 集群即通过 X509 证书认证方式,kubeconfig 本质上是个证书文件。

客户端使用证书中的 Common Name 作为请求的用户名,organization 作为用户组的成员信息。

证书的签发可以使用 openssl、cfssl 等工具来签发,也可以使用 k8s 自带的 CertificateSigningRequest 对象来实现签发。

1.1. CertificateSigningRequest 签发证书

创建私钥信息:

1
2
openssl genrsa -out myuser.key 2048
openssl req -new -key myuser.key -out myuser.csr -subj "/CN=myuser"

创建如下的 CertificateSigningRequest 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat <<EOF | kubectl apply -f -
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: myuser
spec:
# value 使用命令 cat myuser.csr | base64 | tr -d "\n" 获取
request: xxx
# 固定值
signerName: kubernetes.io/kube-apiserver-client
# 过期时间
expirationSeconds: 86400 # one day
usages:
- client auth
EOF

查看 csr 处于 Pending 状态:

1
2
3
$ kubectl get csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
myuser 12s kubernetes.io/kube-apiserver-client kubernetes-admin 24h Pending

批准给证书签发请求:

1
kubectl certificate approve myuser

签发完成后的证书会存放到 status.certificate 字段中,至此证书签发完成。

2. ServiceAccount

使用场景:该方式使用较为常见,用于 pod 中访问 k8s apiserver。

原理:pod 可以通过 spec.serviceAccountName 字段来指定要使用的 ServiceAccount,如果没有指定则使用 namespace 下默认的 default ServiceAccount。kube-controller-manager 中的 ServiceAccount 控制器会在拉起的 pod 中自动注入如下的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spec:
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-j6vpz
readOnly: true
volumes:
- name: kube-api-access-j6vpz
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace

即将信息注入到 pod 的 /var/run/secrets/kubernetes.io/serviceaccount 目录下,目录结构如下:

1
2
3
4
/var/run/secrets/kubernetes.io/serviceaccount
|-- ca.crt -> ..data/ca.crt
|-- namespace -> ..data/namespace
`-- token -> ..data/token

token 为 JWT 认证,对其格式解密后如下:

1
2
3
4
{
"alg": "RS256",
"kid": "u7rF5JCtJRNiMzSUOFAYvDpCwPqUII-N-OtxR59cnQ0"
}
1
2
3
4
5
6
7
8
{
"iss": "kubernetes/serviceaccount",
"kubernetes.io/serviceaccount/namespace": "default",
"kubernetes.io/serviceaccount/secret.name": "ingress-token-s9gtm",
"kubernetes.io/serviceaccount/service-account.name": "ingress",
"kubernetes.io/serviceaccount/service-account.uid": "19ce4f11-7105-43ce-b189-f3d71a2ffc74",
"sub": "system:serviceaccount:default:ingress"
}

2.1. Secret 存放 token

在 1.22 版本及之前版本中,token 以 Secret 的形式存在于 pod 所在的 namespace 下,且 token 不会过期。Secret 的名字存在于 ServiceAccount 的 spec 中,格式如下:

1
2
3
4
apiVersion: v1
kind: ServiceAccount
secrets:
- name: nginx-token-scjvn

而 Secret 通过 Annotation kubernetes.io/service-account.name 指定了关联的 ServiceAccount。

在后续版本中,为了兼容当前方案,如果 ServiceAccount 关联了 Secret,则认为仍然使用 Secret 中存放 token 的方式。如果 Secret 已经很长时间没有使用,则自动回收 Secret。

如果要手工创建一个 token Secret,可以创建如下的 Secret,k8s 自动会为 Secret 产生 token:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
name: build-robot-secret
annotations:
# 带有 annotation
kubernetes.io/service-account.name: build-robot
type: kubernetes.io/service-account-token

2.2. TokenRequest API 产生 token

在 1.22 之后的版本中,kubelet 使用 TokenRequest API 获取有时间限制的临时 token,该 token

会在 pod 删除或者 token 生命周期(默认为 1h)结束后失效。

可以使用 kubectl create token default 来为 ServiceAccount default 创建 token,该命令实际上向 kube-apiserver 发送了请求 /api/v1/namespaces/default/serviceaccounts/default/token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"kind": "TokenRequest",
"apiVersion": "authentication.k8s.io/v1",
"metadata":
{
"creationTimestamp": null
},
"spec":
{
"audiences": null,
"expirationSeconds": null,
"boundObjectRef": null
},
"status":
{
"token": "",
"expirationTimestamp": null
}
}

kube-apiserver 支持如下参数:

  1. –service-account-key-file:用来验证服务账号的 token。
  2. –service-account-issuer:ServiceAccount token 的签发机构。
  3. –service-account-signing-key-file:ServiceAccount token 的签发私钥。

3. 用户伪装

一个用户通过 Http Header Impersonation- 的方式来扮演另外一个用户的身份。

场景:跨 k8s 集群访问的网关服务

支持的 Http Header 如下:

  1. Impersonate-User:要伪装的用户名。
  2. Impersonate-Group:要伪装的组名。该 Header 可以为多个,即支持多个组。

4. bootstrap token

使用 kube-apiserver 参数 --enable-bootstrap-token-auth=true 启用功能,引导 token 以 Secret 的形式存放在 kube-system 下。

该功能仅用于节点初始化时加入到 k8s 集群中。

资料

client 端限流

在 client-go 中会默认对客户端进行限流,并发度为 5。可以通过修改 rest.Conifg 来修改并发度。

MaxInFlightLimit 限流

通过如下参数来控制:

  • –max-requests-inflight:代表只读请求的最大并发量
  • –max-mutating-requests-inflight:代表写请求的最大并发量

该实现为单个 kube-apiserver 层面的,可以针对所有的请求。

EventRateLimit

用来对 Event 类型的对象进行限制,可以通过 kube-apiserver 的参数 –admission-control-config-file 来指定配置文件,文件格式如下:

1
2
3
4
5
6
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: EventRateLimit
path: eventconfig.yaml
...

其中 EventRateLimit 为对 Event 的限制,eventconfig.yaml 文件为详细的对 Event 的限流策略,可以精确到 Namespace 和 User 信息。

1
2
3
4
5
6
7
8
9
10
apiVersion: eventratelimit.admission.k8s.io/v1alpha1
kind: Configuration
limits:
- type: Namespace
qps: 50
burst: 100
cacheSize: 2000
- type: User
qps: 10
burst: 50

API 优先级和公平性

版本状态:

  1. alpha:1.18

通过 kube-apiserver 的参数 --enable-priority-fairness 来控制是否开启 APF 特性。

资料

默认的情况下,k8s 对于 pod 在单个节点的资源分配并不会考虑到 NUMA 架构。比如 cpu 默认会采用 cgroup CFS 来做资源的分配,并未考虑到 NUMA 架构。为了提升 pod 的性能,需要 pod 在分配资源时感知 NUMA 架构。
为此,k8s 在 kubelet 中通过 CPU Manager、Memory Manager、Device Manager、Topology Manager 等特性对 NUMA 做了支持,支持的 pod QoS 类型要求为 Granteed pod。
各特性的支持版本情况如下:

特性 alpha beta stable
CPU Manager 1.12 1.26
Memory Manager 1.21 1.22 -
Topology Manager 1.16 1.18 -

CPU Manager

在 k8s 中使用 cgroup 的 CFS 配额来执行 pod 的 CPU 约束,在 CFS 的模式下,pod 可能会运行在不同的核上,会导致 pod 的缓存失效的问题。对于性能要求非常高的 pod,为了提升性能,可以通过 cgroup 中的 cpuset 绑核的特性来提升 pod 的性能。

在 k8s 中仅针对如下的 pod 类型做了绑核操作:

  1. 必须为 guaranteed pod 类型。即 pod 需要满足如下两个条件:
    1. pod 中的每个容器都必须指定cpu 和 内存的 request 和 limit。
    2. pod 中的每个容器的 cpu 和内存的 request 和 limit 必须相等。
  2. pod 的 cpu request 和 limit 必须为整数。

kubelet 的参数配置

在 k8s 中仅通过 kubelet 来支持 pod 的绑核操作,跟其他组件无关。
kubelet 通过参数 --cpu-manager-policy 或者在 kubelet 的配置文件中参数cpuManagerPolicy 来配置,支持如下值:

  1. none:默认策略。即不执行绑核操作。
  2. static:允许为节点上的某些特征的 pod 赋予增强的 cpu 亲和性和独占性。

kubelet 通过参数 --cpu-manager-reconcile-period 来指定内存中的 cpu 分配跟 cgroupfs 一致。
kubelet 通过参数 --cpu-manager-policy-options来微调。该特性通过特性门控 CPUManagerPolicyOptions 来控制。

模式之间切换

默认的 kubelet cpuManagerPolicy 配置为 none,策略配置位于文件 /var/lib/kubelet/cpu_manager_state,文件内容如下:

1
{"policyName":"none","defaultCpuSet":"","checksum":1353318690}

修改 kubelet 的cpuManagerPolicy 为 static,将模式从 none 切换为 static,重启 kubelet。发现 kubelet 会启动失败,kubelet 并不能支持仅修改参数就切换模式,报如下错误:

1
2
Mar 06 15:00:02 iZt4nd5yyw9vfuxn3q2g3tZ kubelet[102800]: E0306 15:00:02.463939  102800 cpu_manager.go:223] "Could not initialize checkpoint manager, please drain node and remove policy state file" err="could not restore state from checkpoint: configured policy \"static\" differs from state checkpoint policy \"none\", please drain this node and delete the CPU manager checkpoint file \"/var/lib/kubelet/cpu_manager_state\" before restarting Kubelet"
Mar 06 15:00:02 iZt4nd5yyw9vfuxn3q2g3tZ kubelet[102800]: E0306 15:00:02.463972 102800 kubelet.go:1392] "Failed to start ContainerManager" err="start cpu manager error: could not restore state from checkpoint: configured policy \"static\" differs from state checkpoint policy \"none\", please drain this node and delete the CPU manager checkpoint file \"/var/lib/kubelet/cpu_manager_state\" before restarting Kubelet"

将文件 /var/lib/kubelet/cpu_manager_state 删除后,kubelet 即可启动成功,新创建的 /var/lib/kubelet/cpu_manager_state 文件内容如下:

1
{"policyName":"static","defaultCpuSet":"0-3","checksum":611748604}

可以看到已经存在了绑核的 pod。

如果节点上已经存在符合绑核条件的 pod,在修改配置并重启 kubelet 后即可绑核生效。

绑核实践

配置 kubelet 的cpuManagerPolicy值为 static,创建 guaranteed pod:

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: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
resources:
limits:
memory: "200Mi"
cpu: "2"
requests:
memory: "200Mi"
cpu: "2"

在 pod 调度的节点上,进入到 /sys/fs/cgroup/cpuset/kubepods.slice 目录下,跟绑核相关的设置均在该目录下,该目录的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ll /sys/fs/cgroup/cpuset/kubepods.slice
-rw-r--r-- 1 root root 0 Mar 6 10:31 cgroup.clone_children
-rw-r--r-- 1 root root 0 Mar 6 10:31 cgroup.procs
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.cpu_exclusive
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.cpus
-r--r--r-- 1 root root 0 Mar 6 10:31 cpuset.effective_cpus
-r--r--r-- 1 root root 0 Mar 6 10:31 cpuset.effective_mems
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.mem_exclusive
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.memory_migrate
-r--r--r-- 1 root root 0 Mar 6 10:31 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.memory_spread_page
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.mems
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 Mar 6 10:31 cpuset.sched_relax_domain_level
drwxr-xr-x 2 root root 0 Mar 6 10:31 kubepods-besteffort.slice
drwxr-xr-x 10 root root 0 Mar 6 10:31 kubepods-burstable.slice
drwxr-xr-x 4 root root 0 Mar 6 15:09 kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice
-rw-r--r-- 1 root root 0 Mar 6 10:31 notify_on_release
-rw-r--r-- 1 root root 0 Mar 6 10:31 pool_size
-rw-r--r-- 1 root root 0 Mar 6 10:31 tasks

其中 kubepods-besteffort.slice 和 kubepods-burstable.slice 分别对应的 besteffort 和 burstable 类型的 pod 配置,因为这两种类型的 pod 并不执行绑核操作,所有的子目录下的 cpuset.cpus 文件均绑定的 cpu 核。
kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice 目录为要绑核的 pod 目录,其中 4dc3ad18_5bad_4728_9f79_59f2378de46e 根据 pod 的 uid 转换而来,pod 的 metadata.uid 字段的值为 4dc3ad18-5bad-4728-9f79-59f2378de46e,即目录结构中将-转换为_
目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ ll /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice
-rw-r--r-- 1 root root 0 Mar 6 15:09 cgroup.clone_children
-rw-r--r-- 1 root root 0 Mar 6 15:09 cgroup.procs
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.cpu_exclusive
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.cpus
-r--r--r-- 1 root root 0 Mar 6 15:09 cpuset.effective_cpus
-r--r--r-- 1 root root 0 Mar 6 15:09 cpuset.effective_mems
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.mem_exclusive
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.memory_migrate
-r--r--r-- 1 root root 0 Mar 6 15:09 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.memory_spread_page
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.mems
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 Mar 6 15:09 cpuset.sched_relax_domain_level
drwxr-xr-x 2 root root 0 Mar 6 15:09 cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope
drwxr-xr-x 2 root root 0 Mar 6 15:09 cri-containerd-cce0a338829921419407fcdc726d1a4bd5d4489da712b49a4834a020131ce718.scope
-rw-r--r-- 1 root root 0 Mar 6 15:09 notify_on_release
-rw-r--r-- 1 root root 0 Mar 6 15:09 pool_size
-rw-r--r-- 1 root root 0 Mar 6 15:09 tasks

其中 cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope 和 cri-containerd-cce0a338829921419407fcdc726d1a4bd5d4489da712b49a4834a020131ce718.scope 为 pod 的两个容器,其中一个为 pause 容器,另外一个为 nginx 容器。

1
2
$ crictl pods | grep nginx-deploy
cce0a33882992 6 hours ago Ready nginx-deployment-67778646bb-mgcpg default 0 (default)

其中第一列的 cce0a33882992 为 pod id,cri-containerd-cce0a338829921419407fcdc726d1a4bd5d4489da712b49a4834a020131ce718.scope 对应的为 pause 容器,查看该目录下的 cpuset.cpus 文件,绑定了所有的核,并未做绑核操作。

1
2
$ crictl ps | grep nginx-deploy
75f8e4b4185f6 e4720093a3c13 6 hours ago Running nginx 0 cce0a33882992 nginx-deployment-67778646bb-mgcpg

其中第一列的 75f8e4b4185f6 为 container id,cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope 对应的为 nginx 容器。查看 /sys/fs/cgroup/cpuset/kubepods.slice/kubepods-pod4dc3ad18_5bad_4728_9f79_59f2378de46e.slice/cri-containerd-75f8e4b4185f673869604d300629ec2de934cf253244bf91c577f9fc0ba0f14a.scope/cpuset.cpus 对应的值为2-3,说明绑核成功。

/var/lib/kubelet/cpu_manager_state 文件内容如下,跟 cgroup cpuset 实际配置可以完全对应:

1
{"policyName":"static","defaultCpuSet":"0-1","entries":{"4dc3ad18-5bad-4728-9f79-59f2378de46e":{"nginx":"2-3"}},"checksum":3689800814}

kubelet 内部实现

在 kubelet 内部,采用 cpu manager 模式实现绑核功能。

总结

  1. 只能 by 节点配置,不能按照 pod 来灵活的配置。
  2. 无法适用于所有类型的 pod,pod 必须为 Guaranteed 时才允许开启。
  3. 修复配置较为麻烦,一旦配置变更后,需要删除文件 /var/lib/kubelet/cpu_manager_state 后才可生效,而且对现有的 pod 均有影响。

Memory Manager

kubelet 中参数配置

k8s 1.21 版本引入,需要使用 featuregate 开启,参数 --feature-gates=MemoryManager=true。在k8s 1.22 版本为 beta 版本,默认开启。

kubelet 通过参数 --memory-manager-policy 来配置内存管理策略,支持如下值:

  1. none:默认策略,不执行任何内存分配的策略。
  2. static:仅针对 Guaranteed pod 生效,对于 Guaranteed pod,会返回跟 NUMA 相关的 topology hint 信息。在该模式下,会修改 cgroup cpuset.mems 的配置为对应的 cpu core。

kubelet 将已经分配的 pod 的内存绑定信息位于文件 /var/lib/kubelet/memory_manager_state 中,文件格式为 json。

Topology Manager

Topology Manager 特性理解起来比较抽象。举个例子说明:上述的 CPU Manager 和 Memory Manager 的特性,在 kubelet 的实现中是完全独立的,可能会导致 cpu 和内存被分配到了不同的 numa 节点上,CPU Manager 通过 cgroup 的 cpuset.cpus 来控制容器要绑定的 cpu,而 Memory Manager 则通过 cgroup 的 cpuset.mems 来控制容器要使用的 NUMA node 内存。如果两者的信息不匹配,则会导致跨 NUMA Node 的内存访问,从而会对于性能要求高的应用产生影响。
Topology Manager 是 kubelet 中的一部分功能,通过 Hint Providers 来发送和接收各个模块的 NUMA 拓扑信息,比如接收 CPU Manager 和 Memory Manager 的 NUMA Node以及 NUMA Node 分配的优先级。

kubelet 的参数配置

kubelet 通过参数--topology-manager-scope来指定作用域:

  1. container:默认值,按照容器级别分配到共同的 NUMA node 集合上。
  2. pod:将 pod 内的所有 container 分配到共同的 NUMA node 集合上。

kubelet 通过参数 --topology-manager-policy 来设置 NUMA 的分配策略:

  1. none:默认值,不执行任何的拓扑对齐。
  • best-effort:优先选择首选亲和性的 NUMA node,如果亲和性不满足,pod 仍然可以调度成功。
  • restricted:选择首选亲和性的 NUMA node,如果亲和性不满足,pod 调度失败。
  • single-numa-node:通过 Hint Provider 返回的结果,判断单 NUMA 节点的亲和性是否,如果不满足,则 pod 调度失败。

kubelet 中的实现

在 kubelet 中定义了接口:

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
// TopologyHint is a struct containing the NUMANodeAffinity for a Container
type TopologyHint struct {
// 记录了 NUMA Node 满足资源请求的位掩码
NUMANodeAffinity bitmask.BitMask
// Preferred is set to true when the NUMANodeAffinity encodes a preferred
// allocation for the Container. It is set to false otherwise.
// 亲和性的结果是否为首选的
Preferred bool
}

// HintProvider is an interface for components that want to collaborate to
// achieve globally optimal concrete resource alignment with respect to
// NUMA locality.
type HintProvider interface {
// GetTopologyHints returns a map of resource names to a list of possible
// concrete resource allocations in terms of NUMA locality hints. Each hint
// is optionally marked "preferred" and indicates the set of NUMA nodes
// involved in the hypothetical allocation. The topology manager calls
// this function for each hint provider, and merges the hints to produce
// a consensus "best" hint. The hint providers may subsequently query the
// topology manager to influence actual resource assignment.
GetTopologyHints(pod *v1.Pod, container *v1.Container) map[string][]TopologyHint
// GetPodTopologyHints returns a map of resource names to a list of possible
// concrete resource allocations per Pod in terms of NUMA locality hints.
GetPodTopologyHints(pod *v1.Pod) map[string][]TopologyHint
// Allocate triggers resource allocation to occur on the HintProvider after
// all hints have been gathered and the aggregated Hint is available via a
// call to Store.GetAffinity().
Allocate(pod *v1.Pod, container *v1.Container) error
}

CPU Manager、Memory Manager 和 Device Manager 均实现了该接口。在 Topology Manager 中根据各个 Manager 返回的 TopologyHint 数据,从而决定最终的 NUMA Node 分配,并调用各个 Manager 的 Allocate 来做最终的 NUMA Node 分配。

资料

kubeconfig 文件结构

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
apiVersion: v1
kind: Config
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJME1ETXlNVEE0TXpZMU1Wb1hEVE0wTURNeE9UQTRNelkxTVZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTFBaCjZUcUFNMVhiRkxIbnVvd1ZNT1FHeWQ2SzVBcGdwcmhYSlBvclVKdStoazBKd3BQQlNRZGNSdnJjYy9wNTNDbnQKTTBXWVN4dThTd1Z6a0dHajh0cHNSNjRkMWMxdFk1djYzYnlkVXN5M3JwME1OWUt1ckJPNEY2aVFLK01oL3R6UAp4eUdZei9BUnhheDdXNysvWEV6Y2FsdFp5T1JZZk9ISUR5ZjN5R3V2T1htYmw5ZEJmRHFBMFlSMGxOTFFlUEcrCm5lZkVGb1dUQncxUytiZEl0MDBRZnl3MVlvRXpkOFd6UDRBTzFlV3AxK0tJdFhLaUxyaWFBbjkzNTJhbnVIT2YKQ3h0U0NCbEwwRThCL3dGKzhTd0RQbDlUYkNhZU1nTUJpY2hsQzlYSjNrV2k2cElOVnVEeEpRUy82cnlwQWhLbgpHK21RWlJIdmhsUk1wTjhEbDBzQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZLay8zWDh0Q1dhZEpKN011SjNRNE5DL2xHd3pNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBSDR1K05zbE9ORWJVZXB1M0JaRQozL1hnQitFNDZVZ3N2d2R6UzhEWXF3YzBWQ28vUFc0RUNreW9IUkJINHJkdTVpaTBhVUhjZUxpNjhZQjlvQ3hpCmlkbHp3bUNGV1g1dEtGMUJTRFJ1YSt5MjVrOGZuSGtodm5IVG9CQ2c5ZlMzdFBOekNUdEtMSUhyVFpQVDhWU3IKdVYyZkdOcXMyT2djWjc4TmY4M2pnVVBXbFVPZkVsTDBrYjNYVHo5M29DdnF0RS9tRi9VOWhmOUdiRU5Lai9BSAovUHA4QWNmellkVGxGNTBva09temVFdnJnalZmaXFKMitGd2I5Lys1SUljSWFMR3IwZjVUMUVINzdxVEdEWElWCncwWGlKYmVTL1JTYTYrZFM0b1dRMS82Ryt1SUdRQWx0eTRSTXdqbG4rMTMvdStPelowTHErNzBlQTdUSm5mU0MKRVhBPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
server: https://127.0.0.1:55282
name: kind-kind
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURsRENDQW55Z0F3SUJBZ0lVQVFVeTNtWi80eWZmbEhjTi9WblNIcUYwL0NRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1lqRUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdUQWtOQk1SSXdFQVlEVlFRSEV3bFRkVzV1ZVhaaApiR1V4RURBT0JnTlZCQW9UQjB0MVltVmFiMjh4Q3pBSkJnTlZCQXNUQWtOQk1STXdFUVlEVlFRREV3cExkV0psCmNtNWxkR1Z6TUI0WERUSTBNRE15TVRBNE16a3dNRm9YRFRJNU1ETXlNREE0TXprd01Gb3dZakVMTUFrR0ExVUUKQmhNQ1ZWTXhDekFKQmdOVkJBZ1RBa05CTVJJd0VBWURWUVFIRXdsVGRXNXVlWFpoYkdVeEVEQU9CZ05WQkFvVApCMHQxWW1WYWIyOHhDekFKQmdOVkJBc1RBa05CTVJNd0VRWURWUVFERXdwTGRXSmxjbTVsZEdWek1JSUJJakFOCkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXlNNGo2d1NSdTUrcFpwckk1WkhxdmYycXpJdTUKSlZkYzRFUEp3cGY5eU9LN1FPZFM1SHJRUzVNK3ZEMFMzOUNJVFVYY2YxZUNJZy83SEt4TmVFbVk0TVdCMVlBWQpZOVRjMkZlR3JMMVBFR0lwNER6TnRRMkhvcWFxU2pJd0d0bnh3RjV5OGRRUGJkQ3JOdllDRVl5QlR2S0VtUXVoClFmdDhJR1NmaWJ0M3gwa2ZaaUFqZTJ5SDJabFNyMnBRSzRWWFdWUU5UV0hOQnlMS29Lb05Yazl2UTQ4dHhYbVUKcFRDWUxjcGdZSC9tU0lpY1FOcDQwRjRaOUUraGFjdTVkYVFVakIzZzQxWEVvYXBzL2xSa3d0bVFlV1gwVjR2VQp3cUlCS0doZmJ3dmluUjAvTmJGOVVLVldaVlpVL2R0NHR6TXQxVHg4L2tmcExmL0xnNXdoOThUSkJ3SURBUUFCCm8wSXdRREFPQmdOVkhROEJBZjhFQkFNQ0FRWXdEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVUKWXp4aGFlN2V3TXNIeEJ5YnQ4U0Ntb01zVGg0d0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFFWk5uUjI5SDVjRwpJSFBKdDlNMk5YZGt1NzFuU2VZb2s3SFBVdnJ6V0JOdFJjMUJPZkJRc25zb0RRcDQ1SmQxUnZsOEdLRHJjOCtICm5yaXZGQXpPZFRPQlhib3RMcFdGc0U1WU5VcGlUbWU2aW9pUVRVTnQ2WCtLd29CV00xNUwyWlJBYXdQQ0FBZ0sKeWRRU2lZaHVZMS93ekltWW1LenczRjVYb1BJcHVjTjhNam1MM3ZPNlFPaW51OFQrcW9wbWlmRFMraUVzRjcwSApwaW9mVXFJR3Zmbm5uVFFTWnFrRFAybXZ2VHFqT1lCbjU1dHRsL2JYQzh2ZHlwakNGVWRGOXNjUFM5R28ycTRuClkySUUvdHIvUFhaODUvUVVsdVQ2UEd5VUMycDNhZmNhdXRBbC9hQUZvM1ltK3BNbW03WUhad2JjcUp6U3BmRXIKbnJaekRNZFNGS2c9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
server: https://127.0.0.1:6443
name: zoo
contexts:
- context:
cluster: kind-kind
user: kind-kind
name: kind-kind
- context:
cluster: zoo
user: zoo-admin
name: zoo
current-context: kind-kind
preferences: {}
users:
- name: kind-kind
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lJSkNhSHFyUUl0b0l3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB5TkRBek1qRXdPRE0yTlRGYUZ3MHlOVEF6TWpFd09ETTJOVEphTURReApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sa3dGd1lEVlFRREV4QnJkV0psY201bGRHVnpMV0ZrCmJXbHVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXFNS1VOcStyTGNPTkxDWmMKTjA4SjBpYzhBdnhjdmlZNG40SVN1UFFleVViYUhLTVpRNnJpNjA4SlZoNUZxc3B3N3BoWTkrYVBMTHJQaG9uWApCQWhQdmVSSVVxeDdaTkJhODNuVUIrTXIrOXIrazAwTk1yNDBTdkg5aGpnTXVZUjFvN051OWRzN1k3U0pOcXVsCnlwOVN6YzUwUTNBbzh1cHBTRFlFRW5Hd3I0VVBidlp0ZTVlQXo2T2hDYy9hazZuZGlFcU9hMkdJRzhlUmEyWTkKM2hBQjl6V3YvVldGTkxFNXh2Mm5oS3JDQVl6TzBrVmJwTkNlSmkxRGFvNTIzQURWajhGQjRFbDFVNCtvTmM2Vwo5VHdRbDFIUklxZXJ6MUFMTFl1THhadGFFY2IwYW04N2dTcEtyaXcrYWVBL3h6L29tQ1BLY1IrQTBSTkpBcVdWCnduTzhDUUlEQVFBQm8xWXdWREFPQmdOVkhROEJBZjhFQkFNQ0JhQXdFd1lEVlIwbEJBd3dDZ1lJS3dZQkJRVUgKQXdJd0RBWURWUjBUQVFIL0JBSXdBREFmQmdOVkhTTUVHREFXZ0JTcFA5MS9MUWxtblNTZXpMaWQwT0RRdjVScwpNekFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBbjVCalc3VEZvYlBxWk5obS82cEJPVUdGUmVncHFKYVcrUFN1CktUVU5qNXdDU1ZjTWg3V2RmQWtBSGxrYzQ5YWZlaHpWZmVwUXJEUEpOam96eCsxOGc2ZmtNalJRdEhRaW9yYSsKck82UklBYnVJR0pqTXBKVGNGL25OMkY4amFzdFlrdkZ1cjNtdlVldzhsWmtEZEdYMFFaU0J2Y0xqVGdvZURmSQpLTmhjMGVaQ2QrMStGeVJYajZwaUs4Y3pBWlkvTTVsVHJSZTVQUmJSaHpMeVo0Wm1nMHZjOUZjZFcxbThVZ2hDCkVOWkZCVDA0WTl2bHRGTHJaSG9IRlFERlNKRUxTSnl5VG95dTVnYVh6OElZUm41Y0k0b1RPZXlKN2JvQ3hvdzEKa3VXbWdxbVZHdjZwUWJqRm0zaTBTZXFRMkQ5bU1SaDhsYW0vMlhiaVlyWlBjQUlaRnc9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcEFJQkFBS0NBUUVBcU1LVU5xK3JMY09OTENaY04wOEowaWM4QXZ4Y3ZpWTRuNElTdVBRZXlVYmFIS01aClE2cmk2MDhKVmg1RnFzcHc3cGhZOSthUExMclBob25YQkFoUHZlUklVcXg3Wk5CYTgzblVCK01yKzlyK2swME4KTXI0MFN2SDloamdNdVlSMW83TnU5ZHM3WTdTSk5xdWx5cDlTemM1MFEzQW84dXBwU0RZRUVuR3dyNFVQYnZadAplNWVBejZPaENjL2FrNm5kaUVxT2EyR0lHOGVSYTJZOTNoQUI5eld2L1ZXRk5MRTV4djJuaEtyQ0FZek8wa1ZiCnBOQ2VKaTFEYW81MjNBRFZqOEZCNEVsMVU0K29OYzZXOVR3UWwxSFJJcWVyejFBTExZdUx4WnRhRWNiMGFtODcKZ1NwS3JpdythZUEveHovb21DUEtjUitBMFJOSkFxV1Z3bk84Q1FJREFRQUJBb0lCQUJzQWR5S0EzUXpIZXpFVApPaklIVFhUNG5odUVNWHFqTnZBZXFjdzZFeXIxVVRTL3krME56SjBGMm1LVEdXYUlXYVZ6YnRqTFpTRXRDc05tCkRxY3doVUhHNHVPSGdYN1I3NXVCWkxHV1laVThwdnIrbXh3Qlh2Q1c0NCswTENVSzBwL010L1pTaTZBYVpOSUEKaU5od3daajRiWlhVdmxpUHRTUytyOHdic0wrRWNqTGFtZVhlQWNpTDByNTVzZzA5akxhSENkNnhQZ29FYlFPawpKR25kZ1JsSTFSZis5RGdON0toRkdQMkEvOGJEMytONERZT3pRcm8zWE1ZcG9kNyt3ODlkclFnOG9ob2pRQkd5CjZrR0tNeXdsaktNRmxuV3c4bmYwbDU4VzVibWo2enp3RzZPcDQra21wVStyc3pqR0JEdDQ1QnlQNUhTbEFEelMKYzNudFArRUNnWUVBeUhGc1l0aHZ4Vk1sVWdzZTViRzVWYXdwUGNjU3JuY0p3SXU4U1AvaExYUi9POUsvZVhwTAo2OUhtemNMSW1Dd1J1cWwzdUEvQlhYR1hpNlh5SWMvK3k0bzZyUEhoZk4wS2x5cFVOTXd4Ri9FY0t2VklmeE1ECjM2Uks5eVdvem5zVUFVVkw5L0dPK2ViK0dLOGtQRHYyVFdwTEVIVDFjVFc4aHNNV3JvTENLclVDZ1lFQTE0a1IKT2FWejBEbHFLUXlNSU0zVkk4Kzc4TUNOTzdUTFVOait4SWRyV2lGLzlCOG8wSVp3dG5GREo1OHE3dWZNcHhpTAptZEtwRENHcGhIWmhRQzYxWEhBNXEyVFIzamxhK1ArdnpTRVNxeWIvWWhrelk3dDdZY0xMSFBValRiWXpmV2I0Cjl6TjdTNkF6NWM2aGs1Nzd3RmxTODE2KzlWK2pBVW5MaWdkZzNJVUNnWUVBb1BlbFNRUHpUbzNsREt2dGxoeFIKYitHZ0JRS1htQS8wZnZJNHRJNzRzRjQ3eHprSmwyNkZCYzQ5QWNTSS90dDFLV2Zxd3ArMGMyeERmVnc0eExxYQpMYTdHVEJpN01tRDRua2paOHNTQU1HL3FaUDB4eVFybU0zVm0xbThoengrOEF3RTVidFpJTVp3MU5uR0FNZmNkClp6SVRNaFlhL1YxZ0Z3RVlkL0IrS1hrQ2dZRUFxdzA1b1dGQVAxRkJnaUJXR1RhaFg1RmVXeHZGT2t3cVN4aGIKWUVjRW1Id2JtdmNib2huLzI1cVpyQmt5cm5WQndwN0ZNNmV1eDFUenZvOWdjTnBnem1LMk1lS0tkKzFXMkdPNgo5blczNWlMRjdPbUpFaTVaSmVXODRsZGQxQyswUDJKNFZWOERDNnF4WlVFT2xDUkpNWWJ5UVBqQlhlU3ZiYmRPCkZGWDB0aTBDZ1lBZDNXTjl2SlN5UUxDUDlHdHA0dVRJQXpYRlFGbkJiY2ZKVzdaL2hnUXlZUWlWa0p6K3pQSCsKclU5RWE3ZTVyTlg0MmtuK2E1cXpudXZEeE5aMDhjRjYwdFBBajR2T1UrTkZyb2FyU29xL3lreTJMTnR2LzJ0OApocXJQblk0dE04dFpON2w5NktHSklJdzlVUXcyR2NpS3JXSXkvT0NQR2lERGthakhGS2lndlE9PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
- name: zoo-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQyRENDQXNDZ0F3SUJBZ0lVYlBFdGx4LzRTYm9TckRvZTd1djllS1YrckZNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1lqRUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdUQWtOQk1SSXdFQVlEVlFRSEV3bFRkVzV1ZVhaaApiR1V4RURBT0JnTlZCQW9UQjB0MVltVmFiMjh4Q3pBSkJnTlZCQXNUQWtOQk1STXdFUVlEVlFRREV3cExkV0psCmNtNWxkR1Z6TUI0WERUSTBNRE15TVRBNE16a3dNRm9YRFRJMU1ETXlNVEE0TXprd01Gb3dhVEVMTUFrR0ExVUUKQmhNQ1ZWTXhDekFKQmdOVkJBZ1RBa05CTVJJd0VBWURWUVFIRXdsVGRXNXVlWFpoYkdVeEZ6QVZCZ05WQkFvVApEbk41YzNSbGJUcHRZWE4wWlhKek1SQXdEZ1lEVlFRTEV3ZExkV0psV205dk1RNHdEQVlEVlFRREV3VmhaRzFwCmJqQ0NBU0l3RFFZSktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU1PYnpoeDBOTW5ubTdHbmFjb2sKWHJ1RmxrRVBXc01la2xLNnBtbWFDRUVVUTNocHdsUWw2Z09UekpwaUVYVGZMZU1Uc29ZZ1NtNGFySWdjdVlTcwo3cDdzdWhhUi9RN1g3SUZjMXJEMzFtODB5amZEVUhYZi9jSWhDVmp0NDhLY1JLL2QrK2NzZkU5eHpEdlVBdDVxCmxmNFNvMy9YSnJyYzdtdURiUXdYdDgvaGQ4RnVZYUFWN2YraXVMMW5DRTB3b01IaFNPSkpEWC9TQ1pMakxTS2wKYU9aM2lOL0dDRXo4cldPUmVBbGUwQzRwazRwc0xmN1h6UlJpcnFTT21lQ3JTZzJoNlU1OS8xTHN5dXVqeTVFWQpxT3RsZ0R0Z3l5RDhBRWZhanM5R0NNRnFFOStHZWR6NkpuRmFlUlpOZThlVWNhVU1GWFVSWjYxb0pqT2UraUZ4CjU3TUNBd0VBQWFOL01IMHdEZ1lEVlIwUEFRSC9CQVFEQWdXZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUIKQmdnckJnRUZCUWNEQWpBTUJnTlZIUk1CQWY4RUFqQUFNQjBHQTFVZERnUVdCQlRPL0JxUE9HcVU1V0JYSW9wQgoxajdxYVBvTkh6QWZCZ05WSFNNRUdEQVdnQlJqUEdGcDd0N0F5d2ZFSEp1M3hJS2FneXhPSGpBTkJna3Foa2lHCjl3MEJBUXNGQUFPQ0FRRUF0SVRaN0FUVVhManB6UWtQVGM0RVFKYi9IZlJTcklOV3pSMlpRcVI1Q2J1dXEzR08KUktDZFphenVrakJjMCtXVkhGcVo4SEtNNUR2YThKbzZCUXgycXoxZ0ptVE1oYUdRMFlyNWFFcmxFZWJCWUcwVgpLMGh4LzJuMmNmMkt3N3VBOWdkeUJKSVFJbnY2RFJPUmt6VVNuQXJEd21TNitUNXdKK0lTUGlPdHVGRnQzazRyClZsZ1N3bjE5WHRSRnJ4OEI4dWRkSlZ4VEloN3FPa09hbURQaFJrTGJKNFdOUk5hYlpVMFVxdmxUQzVnQkhnS2QKSm1FaVQybHNoUENjS2lRZWsrTVNzNU9mNG9ZQml0TXM0eW1iendXb1hVaGcvVnRTbTdtT2MzUmo2ZGhrQXl2NwozQTJoZW1xUENKQ1JwbndzUDlpd0VrWUE0SlFZMG53aEUrd1h2Zz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdzV2T0hIUTB5ZWVic2FkcHlpUmV1NFdXUVE5YXd4NlNVcnFtYVpvSVFSUkRlR25DClZDWHFBNVBNbW1JUmROOHQ0eE95aGlCS2JocXNpQnk1aEt6dW51eTZGcEg5RHRmc2dWeldzUGZXYnpUS044TlEKZGQvOXdpRUpXTzNqd3B4RXI5Mzc1eXg4VDNITU85UUMzbXFWL2hLamY5Y211dHp1YTROdERCZTN6K0Yzd1c1aApvQlh0LzZLNHZXY0lUVENnd2VGSTRra05mOUlKa3VNdElxVm81bmVJMzhZSVRQeXRZNUY0Q1Y3UUxpbVRpbXd0Ci90Zk5GR0t1cEk2WjRLdEtEYUhwVG4zL1V1eks2NlBMa1JpbzYyV0FPMkRMSVB3QVI5cU96MFlJd1dvVDM0WjUKM1BvbWNWcDVGazE3eDVSeHBRd1ZkUkZucldnbU01NzZJWEhuc3dJREFRQUJBb0lCQUhMZ0ZYTndhM0FIck0vdwpXWmgxTTQwOUxyaVdvOTdqSFZ1b2NnS2lpeVp0R0JLblNaRFJrMVQyZjdwS3phV3RTKzJIcTloSkxtenJEVmdDClJwRThYZ2JIVDZIaHFwUUZDc2dPRmFkb1pXNTV1aWgxYzlORjhHa0pyY3VrS1pZbzM4M0l1QjlUYU0zZkx1b1QKNEh0dWJSZ0JLalB4enJUKytxWDVVUmxBOUpvSDc2c1hUb1JkYXF5YzR6VXMwcm9LNmVTb3dZeE8ydVN5TmtQcQpkai9reFJ4WTV2djYveHpPOHhVU1dUWndwVit2VUgvbVJiU1hjSjJrKzRvTDB5VmJSLzFpVStPYmpkSWU4UXNOCkhHMjBNSk9zRS9LTVVuVUlSeGxqRGlaM1o4WjVabXIrTUVTV3daOWRkUWlJbm80dSt6UkVaRTcvbnlwQ2lTY0UKOHlBNG9FRUNnWUVBOVpHbHRLdG8yaHRzZXBuWnNwdFVFdEorbXo2a001bEI2SHdpVVd3Q05ZSXpUQzRra2J2MwpXSzVMYXFpaXEwUk5zOHZSbmNXSHlwMHFidTlyOHNUellRUVVvc0luVStReGlLMUd0Uk81cVV4TE9SZzZlSjViClNCM0JoNnVQdVMyUU9qSlNKbU9uWldkUHFrRXJqSW9LWWZhT00wUGIxaUVlUzBWVUVyVWZaVGNDZ1lFQXkrcmgKeDlzM0FnMVk0bkVpVThsbGpDNlZQYk8yQ3pRM3ViWXJlcjhobVFTYkRsQjBHMEhOVE01YWNaSFdNVUdnWWVlagpUbFJCcVQvbFZtOXV3NFBxc1Ezejl4QnB0YmZDVUpqeEVucnJWQkdpeElNeHlGMU85WTh2WVRkZENLa3VVVVE5CnEybnpuNzNMNGdDRDlMcW4yYlFpaEFucHdTamcySytFQ2V4RlQyVUNnWUVBdWJIM2tsV0VKbHBTZjZ0VG1lSW4KZzB3MWZRT3plMmxMRTVpN0FzTWdNSUpTZENyNGNGT3BTU0FUMjRYRjdLanI4U2dSVExNUWFrREswN1NzOXBuRQpTUHFpK0NqRlFJVHdpQ0F2dGNKQ3hTanlRU3gzR3JyMDMrWFFjTjFsQTJ6WEFZc0g0QXUvaThqQnowY1V2V090ClVrTDFhUUxKZkhUeXlZeVZkTWdPQTZVQ2dZQndDcUY5dDFRVkc1SlA4UXVFYis4TXcvZWFUR2prNVE4TlNpdS8KcU03a0RhVElpNm9QNCtyU25ic1NGYWhUcmhSYVZ2VGlyK2JZQU5TWTFtZE1vK25LMkxqSWNrc3kza0cxR1NPMApITGU2bkdvTGdXNVVBZmpGY2FQOXpYYWZzSjFUWjZSZXo3dGRkT0pXVGlReXpuQTFiUVZkK1RobnVuYzRkOCtiCnlDY1pCUUtCZ0ZWcFZacDdjWUxHYlE1dERkL0orZ1BhcFo0TWFYSzluWDFTaktlalhjcVFFSUc3NEYrdkxzdEwKZCs4R3N0VkRzdy8vQ0VNcW9pYXZ1MkVlQ0VQZzF3ZHpDU3pWa2IrZ1FRZXA0cE1LZEhaai9YaysvNVVXTUJzOApkNnNwMWxrTTRqSGhjYkFLU056VTUrTG1NRnk0MzBPZlI2dmxVTFZjNVlaR2hSU0ZCdzdDCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==

cluster 字段

certificate-authority-data 字段:服务端的 ca 证书,用来验证 kube-apiserver 证书的正确性。
当使用 kubectl 发送请求到 kube-apiserver 时,kube-apiserver 会返回通过参数 --tls-cert-file 配置的证书文件,kubectl 通过 kubeconfig 中的 certificate-authority-data 字段来校验 kube-apiserver 返回证书的有效性。
当 kubectl 指定了参数 --insecure-skip-tls-verify=true,即可跳过对 kube-apiserver 证书的校验。

users

  • name:用户名称
  • client-certificate-data:kubectl 连接 kube-apiserver 时使用的客户端证书,会发送给 kube-apiserver。内容经过 base64 编码。
  • client-key-data:客户端私钥信息。内容经过 base64 编码。

client-certificate-data 对应的为用户的公钥信息,使用命令 echo 'xx' | base64 -d > /tmp/client.crt; openssl x509 -in /tmp/client.crt -noout -text可对证书的内容进行解密。解密完成后的证书内容如下:

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
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 2604918601715070594 (0x242687aab408b682)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN = kubernetes
Validity
Not Before: Mar 21 08:36:51 2024 GMT
Not After : Mar 21 08:36:52 2025 GMT
Subject: O = system:masters, CN = kubernetes-admin
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:a8:c2:94:36:af:ab:2d:c3:8d:2c:26:5c:37:4f:
09:d2:27:3c:02:fc:5c:be:26:38:9f:82:12:b8:f4:
1e:c9:46:da:1c:a3:19:43:aa:e2:eb:4f:09:56:1e:
45:aa:ca:70:ee:98:58:f7:e6:8f:2c:ba:cf:86:89:
d7:04:08:4f:bd:e4:48:52:ac:7b:64:d0:5a:f3:79:
d4:07:e3:2b:fb:da:fe:93:4d:0d:32:be:34:4a:f1:
fd:86:38:0c:b9:84:75:a3:b3:6e:f5:db:3b:63:b4:
89:36:ab:a5:ca:9f:52:cd:ce:74:43:70:28:f2:ea:
69:48:36:04:12:71:b0:af:85:0f:6e:f6:6d:7b:97:
80:cf:a3:a1:09:cf:da:93:a9:dd:88:4a:8e:6b:61:
88:1b:c7:91:6b:66:3d:de:10:01:f7:35:af:fd:55:
85:34:b1:39:c6:fd:a7:84:aa:c2:01:8c:ce:d2:45:
5b:a4:d0:9e:26:2d:43:6a:8e:76:dc:00:d5:8f:c1:
41:e0:49:75:53:8f:a8:35:ce:96:f5:3c:10:97:51:
d1:22:a7:ab:cf:50:0b:2d:8b:8b:c5:9b:5a:11:c6:
f4:6a:6f:3b:81:2a:4a:ae:2c:3e:69:e0:3f:c7:3f:
e8:98:23:ca:71:1f:80:d1:13:49:02:a5:95:c2:73:
bc:09
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Client Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Authority Key Identifier:
A9:3F:DD:7F:2D:09:66:9D:24:9E:CC:B8:9D:D0:E0:D0:BF:94:6C:33
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
9f:90:63:5b:b4:c5:a1:b3:ea:64:d8:66:ff:aa:41:39:41:85:
45:e8:29:a8:96:96:f8:f4:ae:29:35:0d:8f:9c:02:49:57:0c:
87:b5:9d:7c:09:00:1e:59:1c:e3:d6:9f:7a:1c:d5:7d:ea:50:
ac:33:c9:36:3a:33:c7:ed:7c:83:a7:e4:32:34:50:b4:74:22:
a2:b6:be:ac:ee:91:20:06:ee:20:62:63:32:92:53:70:5f:e7:
37:61:7c:8d:ab:2d:62:4b:c5:ba:bd:e6:bd:47:b0:f2:56:64:
0d:d1:97:d1:06:52:06:f7:0b:8d:38:28:78:37:c8:28:d8:5c:
d1:e6:42:77:ed:7e:17:24:57:8f:aa:62:2b:c7:33:01:96:3f:
33:99:53:ad:17:b9:3d:16:d1:87:32:f2:67:86:66:83:4b:dc:
f4:57:1d:5b:59:bc:52:08:42:10:d6:45:05:3d:38:63:db:e5:
b4:52:eb:64:7a:07:15:00:c5:48:91:0b:48:9c:b2:4e:8c:ae:
e6:06:97:cf:c2:18:46:7e:5c:23:8a:13:39:ec:89:ed:ba:02:
c6:8c:35:92:e5:a6:82:a9:95:1a:fe:a9:41:b8:c5:9b:78:b4:
49:ea:90:d8:3f:66:31:18:7c:95:a9:bf:d9:76:e2:62:b6:4f:
70:02:19:17

其中的 Subject 中的 O 对应的 k8s 中的 Group,CN 对应的 k8s 中的 User。kube-apiserver 会通过证书的 O 和 CN 获取到 User 和 Group 信息。在 k8s 系统中,实际上并没有存储 Group 和 User 信息,而是完全依赖该证书中的信息。

kubeconfig 文件生成

kubeconfig 文件本质上是个证书,包含了 ca、证书公钥和证书私钥,在有了证书后可以通过 kubectl 命令生成新的 kubeconfig 文件

1
2
3
4
kubectl --kubeconfig ~/.kube/111111.kubeconfig config set-cluster hello --certificate-authority=/tmp/ca.pem --embed-certs=true --server=https://127.0.0.1:6443
kubectl --kubeconfig ~/.kube/111111.kubeconfig config set-credentials hello-admin --client-certificate=/tmp/tls.crt --client-key=/tmp/tls.key --embed-certs=true
kubectl --kubeconfig ~/.kube/111111.kubeconfig config set-context hello --cluster=hello --user=hello-admin
kubectl --kubeconfig ~/.kube/111111.kubeconfig config use-context hello

使用 curl 命令直接访问 kube-apiserver

由于 kube-apiserver 开启了双向认证,使用 curl 命令访问 kube-apiserver 时,curl 需要指定证书信息,证书信息可以使用 kubeconfig 中的证书信息。

1
2
3
4
5
6
7
8
WORK_DIR=/tmp
KUBECONFIG=~/.kube/config
CONTEXT=kind-kind
server=`yq eval '.clusters.[]|select(.name=="'$CONTEXT'")|.cluster.server' $KUBECONFIG`
yq eval '.users.[]|select(.name=="'$CONTEXT'")|.user.client-certificate-data' $KUBECONFIG | base64 --decode > ${WORK_DIR}/client.crt
yq eval '.users.[]|select(.name=="'$CONTEXT'")|.user.client-key-data' ~/.kube/config | base64 --decode > ${WORK_DIR}/client.key
yq eval '.clusters.[]|select(.name=="'$CONTEXT'")|.cluster.certificate-authority-data' $KUBECONFIG | base64 --decode > ${WORK_DIR}/ca.crt
curl --cert ${WORK_DIR}/client.crt --key ${WORK_DIR}/client.key --cacert ${WORK_DIR}/ca.crt "$server/apis/apiextensions.k8s.io/v1/customresourcedefinitions?limit=500&resourceVersion=0"

资料

0%