清华大学 TUNA 协会的同学们最近推出了一个新的镜像站:OpenTUNA。这个镜像站(看起来)是与 AWS 中国合作的,使用 CloudFront CDN 进行分发,速度 & 稳定性都十分有保证,从此教育网外的朋友们在使用镜像站时又多了一个新的选择。
不过让我关注的是,开站公告里这么提到:

OpenTUNA 采用了 IaC(Infrastructure as Code)的方案,它的架构代码已经开源在 tuna/opentuna 中,可供参考。

基础设施即代码

何谓基础设施即代码?基础设施就基础设施,代码就代码,两者为何能产生联系呢?
过去我们经常说一句话叫 it works on my machine!,一个 App 可能在一台电脑上能正常运行,跑到另一台电脑上可能就不行了。容器技术很好地解决了这个问题中的大部分症结,通过写一个 Dockerfile 来定义应用运行的操作系统环境(依赖项等等),使得每次运行时都是一个符合预期的环境(而不需要手工去安装依赖项、手工解决同一机器上不同版本依赖项的冲突),如果被应用自身污染了就重建容器即可,不会对外部造成任何干扰。
那如果我们更进一步,容器技术总归需要一个宿主机,那这些宿主机(例如一台云上的 VM)能不能也免除手工配置之苦呢?设想我们每开一台虚拟机,都要挂载一遍系统安装镜像,开始冗长的系统安装过程,手动配置用户账户、网络、磁盘分区、软件源,系统安装完后再安装各种需要的软件、配置防火墙规则……这么一下来一两个小时就过去了。如果你只是想开那么几台虚拟机,似乎还可以接受,万一如果你的业务需要几十台、几百台、甚至几百万台呢?
GCP 对基础设施即代码的一句话概括:自动完成预配、配置和部署等重复性任务,无论是一台还是数百万台机器。
什么意思?就是写一个基础设施的 Dockerfile,预先在这份“代码”里定义好基础设施的“样子”,比如安装什么操作系统,需要配置的账户、网络、软件源、预装软件、防火墙,然后通过 Terraform 这样的部署工具,即可使用这些预定义的配置创建几台、几十台、甚至几百万台虚拟机。除了可以配置虚拟机,还可以配置 VPC、负载均衡等等其他云服务商提供的服务。
第一代的 IaC 解决了如何创建资源的问题,例如 Ansible 可以帮你批量在许多台机器上执行命令来完成基础配置,但是并没有对基础设施的生命周期进行管理。一旦基础设施被内部或外部修改,就不符合原先“代码”里定义的预期。为了实现生命周期的管理(创建、更新、销毁),我们就需要维护一份状态。Terraform 这样的工具就将“声明式语言”与状态管理结合在了一起,在每次 terraform apply 时,Terraform 会去检查当前的基础设施状态与预期状态存在的差别,然后决定是要创建、更新还是销毁。(是不是有 Kubernetes 内味了

OpenTUNA 是怎么做的

尝试过配置 tunasync 的同学可能知道,tunasync 分为 master 和 worker 两个部分,前者很容易使用容器化手段来部署,而后者由于其本身也需要调用 Docker 去启动容器,所以容器化就显得不那么好管理。
所以对于 tunasync 的部署,OpenTUNA 项目使用的是 EC2 作为宿主(也就是虚拟机),定义基准镜像(Amazon Linux),指定 vpc 和 role。之后加上一个 Auto Scaling Group(其实有点像 k8s 的 Deployment),指定 scale 和 updateType(滚动升级),就完成了虚拟机的创建、更新、销毁的自动化管理。

  1. const tunaManagerASG = new autoscaling.AutoScalingGroup(this, `${usage}ASG`, {
  2. instanceType: ec2.InstanceType.of(ec2.InstanceClass.C5, ec2.InstanceSize.LARGE),
  3. machineImage: ec2.MachineImage.latestAmazonLinux({
  4. generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
  5. }),
  6. vpc: props.vpc,
  7. userData: ec2.UserData.custom(Mustache.render(userdata, newProps)),
  8. role: ec2Role,
  9. notificationsTopic: props.notifyTopic,
  10. minCapacity: 1,
  11. maxCapacity: 1,
  12. healthCheck: autoscaling.HealthCheck.elb({ grace: cdk.Duration.seconds(180) }),
  13. updateType: autoscaling.UpdateType.ROLLING_UPDATE,
  14. cooldown: cdk.Duration.seconds(30),
  15. });

那么最关键的问题是,怎么设置这个虚拟机的操作系统环境?tunasync 是如何部署在虚拟机上的?留意第 7 行的 userData,它便是魔法的核心所在。这个 userData 长什么样,我们继续看代码:

  1. Content-Type: multipart/mixed; boundary="//"
  2. MIME-Version: 1.0
  3. --//
  4. Content-Type: text/cloud-config; charset="us-ascii"
  5. MIME-Version: 1.0
  6. Content-Transfer-Encoding: 7bit
  7. Content-Disposition: attachment; filename="cloud-config.txt"
  8. #cloud-config
  9. repo_update: true
  10. repo_upgrade: all
  11. packages:
  12. - nfs-utils
  13. - amazon-efs-utils
  14. - amazon-cloudwatch-agent
  15. # run commands
  16. runcmd:
  17. - file_system_id_1={{&fileSystemId}}
  18. - efs_mount_point_1=/mnt/efs/opentuna
  19. - mkdir -p "${efs_mount_point_1}"
  20. - test -f "/sbin/mount.efs" && echo "${file_system_id_1}:/ ${efs_mount_point_1} efs tls,_netdev" >> /etc/fstab || echo "${file_system_id_1}.{{&regionEndpoint}}:/ ${efs_mount_point_1} nfs4 nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,_netdev 0 0" >> /etc/fstab
  21. - test -f "/sbin/mount.efs" && echo -e "\n[client-info]\nsource=liw" >> /etc/amazon/efs/efs-utils.conf
  22. - mount -a -t efs,nfs4 defaults
  23. - tunaversion=v0.7.0
  24. - tunafile="${efs_mount_point_1}/tunasync/install/tunasync-linux-amd64-bin-${tunaversion}.tar.gz"
  25. - (test -f ${tunafile} && tar -xf ${tunafile} -C /usr/local/bin/) || (wget -t 20 --retry-connrefused -w 5 -T 10 -c https://github.com/tuna/tunasync/releases/download/${tunaversion}/tunasync-linux-amd64-bin.tar.gz -O - | tar xzf - -C /usr/local/bin/)
  26. cloud_final_modules:
  27. - [scripts-user, always]
  28. --//
  29. Content-Type: text/x-shellscript; charset="us-ascii"
  30. MIME-Version: 1.0
  31. Content-Transfer-Encoding: 7bit
  32. Content-Disposition: attachment; filename="userdata.txt"
  33. #!/bin/bash -xe
  34. mkdir -p /etc/tunasync/
  35. mkdir -p /mnt/efs/opentuna/tunasync/
  36. export AWS_DEFAULT_REGION={{&region}}
  37. # setup tunasync manager config
  38. cat > /etc/tunasync/manager.conf << EOF
  39. debug = false
  40. [server]
  41. addr = "0.0.0.0"
  42. port = {{&port}}
  43. ssl_cert = ""
  44. ssl_key = ""
  45. [files]
  46. db_type = "redis"
  47. db_file = "redis://{{&redisHost}}/"
  48. ca_cert = ""
  49. EOF
  50. # create tunasync service
  51. cat > /usr/lib/systemd/system/tunasync.service << EOF
  52. [Unit]
  53. Description=Tunasync Manager daemon
  54. [Service]
  55. ExecStart=/usr/local/bin/tunasync manager -config /etc/tunasync/manager.conf
  56. ExecReload=/bin/kill -HUP \$MAINPID
  57. Type=simple
  58. KillMode=control-group
  59. Restart=on-failure
  60. RestartSec=20s
  61. StandardOutput=syslog
  62. StandardError=syslog
  63. SyslogIdentifier=tunasync
  64. [Install]
  65. WantedBy=multi-user.target
  66. EOF
  67. cat > /etc/rsyslog.d/tunasync.conf << EOF
  68. if \$programname == 'tunasync' then /var/log/tunasync.log
  69. & stop
  70. EOF
  71. # start tunasync service
  72. systemctl daemon-reload
  73. systemctl restart rsyslog
  74. systemctl enable tunasync.service
  75. systemctl start tunasync.service
  76. # configure conf json of CloudWatch agent
  77. mkdir -p /opt/aws/amazon-cloudwatch-agent/etc/
  78. aws s3 cp {{&cloudwatchAgentConf}} /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
  79. # start cloudwatch agent
  80. /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json -s &
  81. --//

这就是传说中的 Cloud-init 配置文件~ 官网称其为 The standard for customising cloud instances。简单来说,在创建虚拟机时只需要指定符合标准的 cloud images 作为系统盘,然后往云服务商提供的 Cloud-init 接口(比如 CD-ROM)写入一个 Cloud-init 配置文件,那么在虚拟机初次启动时就会自动地根据这个配置文件去初始化各种各样的配置。例如我们在阿里云上开一台 Ubuntu 虚拟机,等待约一分钟就能使用 SSH 去连接这台机器,并且已经设置好了网络、hostname 等等。这就是因为阿里云在创建这台虚拟机时提供了 Cloud-init 配置文件。而用户自己也可以提供一份配置文件,这份配置文件叫做 user-data,会与云服务商提供的配置文件合并喂给新的虚拟机。
Cloud-init 可以做的事情有很多,官网上给出的一些 user-data 例子:https://cloudinit.readthedocs.io/en/latest/topics/examples.html

那么 tunasync 是如何部署在上边的?首先是请 cloud-init 更新当前的软件源、升级系统内已有的软件,再安装需要的依赖项( nfs-utils 等等)。之后开始挂载一块 efs 云盘,并从多个位置下载并安装 tunasync。以上的脚本是一次性的,仅在初次开机时执行。同时这个配置文件里又包含了一个 user-script,设置为每次开机时均会运行,这个脚本所做的工作便是配置 tunasync master 并启动它。

Immutable

那你可能会觉得怪怪的,如果我们的配置变动了怎么办?正如容器是 Immutable,我们 provision 出来的基础设施也是 Immutable,所以当配置变更时很大概率是销毁重建~
这也就带来了一个问题。如果我们在自动化创建出来的基础设施上进行了手工的操作,例如使用 kubeadm 安装一个 Kubernetes 集群,那万一我们要调整基础设施的配置,此时导致了基础设施销毁重建,那我们辛辛苦苦创建出来的 Kubernetes 集群不就没了?而这也是目前我对 IaC 还存有疑虑的地方,因为容器的重建成本十分低,但基础设施的重建成本就高了许多。
可是,那就真的没有办法了吗?既然你要自动化,就应该避免去手工执行“破坏”基础设施的操作。过往我们考虑手工使用 kubeadm 创建集群,是因为在我们的经验里,kubeadm 是交互式的,并且 master 节点创建后需要手工记录加入集群的 token。而现在这个问题已经有了一个较好的解决方法:https://cluster-api.sigs.k8s.io/

参考文章

https://insights.thoughtworks.cn/infrastructure-as-code-and-cloud-computing/