需求整理
微软在19年的build大会上公开了Visual Studio Online,相当于把Visual Studio Code和我们需要的开发相关文件装进了浏览器,随时访问。不过目前的公共预览版还没有提供在国内的服务器,定价上最基本的配置4核/8GB RAM/64GB HDD每活跃一小时需3.15元人民币,待机一小时0.05元人民币。自建服务器也必须跑在Azure上,也并不算便宜:https://azure.microsoft.com/zh-cn/pricing/details/visual-studio-online/
社区也开发了相同功能的开源软件:code-server。不过如果我们专程为这个需求分配一个开发机,即使是2核/4GB RAM/40GB SSD的基本机型,不打折时包年的费用也接近2000元人民币。所以使用code-server,我们也必然需要实现按需分配。接下来我们就在阿里云上实现这个需求。
code-server用到了service worker,在不通过localhost访问时,必须使用https协议。所以我们为了实现整个需求,必须用到阿里云的如下服务:
- 一个已经备案的域名,如果用境外服务器的话域名可以不备案
- 一个弹性公网IP,需要的时候申请下来,将一个二级域名code.example.com解析到该IP上
- 容器镜像服务,方便我们快速地使用打包好的code-server docker镜像
- 弹性容器实例ECI,这是最为经济的算力资源,2核/4GB RAM每小时0.44元人民币,4核/8GB RAM每小时0.88元人民币,而且计价是精确到秒的。
- 文件存储服务NAS,我们之后需要将它作为NFS Volume挂载在ECI上,存储开发常用资料。这部分需要长期运行,价格0.30元/GB/月。
实现我们想要的按需分配code-server的“算法”描述起来如下:
- 准备阶段
- 准备好code-server的镜像上传到阿里云容器镜像仓库,镜像里需要有SSL证书相关服务
- 申请好NAS实例
- 需要真正使用时
- 申请一个弹性公网IP x.x.x.x
- 更新二级域名code.example.com的解析到x.x.x.x
- 申请弹性容器实例,以准备好的docker image启动,绑定弹性IP x.x.x.x
- 启动时执行代码,给二级域名code.example.com获取SSL证书后,之后便可以启动code-server了
- 挂载NAS,阿里云这里应该是有些bug,启动时挂载NAS容易使ECI无法正常启动
docker镜像准备
我们首先要准备一个帮助我们处理SSL证书的增强版code-server docker image。
一个思路是,code-server为我们提供了它的Dockerfile,我们可以对这个Dockerfile稍加修改,以满足我们所要的功能。可惜的是,我自己试了多次,即使不更改这个Dockerfile,也无法正确地生成docker image。会遇到这个问题:https://github.com/cdr/code-server/issues/1380
于是我转换了另一个解决方案,基于centos镜像,在这个基础上,下载code-server的Binary Release,布置好SSL证书相关软件,这里选择Let's Encrypt的Certbot。
基于此,准备好的Dockerfile如下(注意这个Dockerfile后面有更新):
FROM centos
RUN cd home \
&& yum -y update \
&& yum -y install wget \
&& wget https://dl.eff.org/certbot-auto \
&& mv -f certbot-auto /usr/local/bin/certbot-auto \
&& chown root /usr/local/bin/certbot-auto \
&& chmod 0755 /usr/local/bin/certbot-auto \
&& wget https://github.com/cdr/code-server/releases/download/2.1698/code-server2.1698-vsc1.41.1-linux-x86_64.tar.gz \
&& tar -xvf code-server2.1698-vsc1.41.1-linux-x86_64.tar \
&& rm -f code-server2.1698-vsc1.41.1-linux-x86_64.tar \
&& mv code-server2.1698-vsc1.41.1-linux-x86_64 code-server \
EXPOSE 8080 80 443
ENTRYPOINT ["tail", "-f", "/dev/null"]
更改工作目录到这个文件夹后,制作docker image:
docker image build -t my-code-server:0.1 .
成功之后查看本地images,找到需要的ID
docker images
登录阿里云的容器镜像服务,这里我选择离我最近的成都节点:
sudo docker login --username=mayundaddy registry.cn-chengdu.aliyuncs.com
tag并推送这个容器镜像:
sudo docker tag [imageID] registry.cn-chengdu.aliyuncs.com/mayundaddy/code-server:0.1
sudo docker push registry.cn-chengdu.aliyuncs.com/mayundaddy/code-server:0.1
测试一下,我们手动做好其他部分的工作,启动这个容器的时候,运行以下指令,成功之后就可以在任意设备上访问了:
/usr/local/bin/certbot-auto certonly --standalone --non-interactive --agree-tos -m my@email.com -d code.example.com && export PASSWORD=simpepassword && /home/code-server/code-server --port 443 --cert /etc/letsencrypt/live/code.example.com/fullchain.pem --cert-key /etc/letsencrypt/live/code.example.com/privkey.pem || tail -f /dev/null
不过这个方法相当于是每次启动时都申请了一个新的SSL证书,Let's Encrypt对此是有频率限制的,整个一级域名每周50个。如果启动次数没有那么频繁,这个也能将就用了。我这边后来为了解决这个问题,其实用了一个常在线的服务器不断维系一个通用*.example.com的SSL证书并且在启动时拷贝过来。
由此我更改了Dockerfile如下:
FROM centos
RUN cd home \
&& yum -y update \
&& yum -y install wget \
zip \
&& wget https://github.com/cdr/code-server/releases/download/2.1698/code-server2.1698-vsc1.41.1-linux-x86_64.tar.gz \
&& tar -xvf code-server2.1698-vsc1.41.1-linux-x86_64.tar \
&& rm -f code-server2.1698-vsc1.41.1-linux-x86_64.tar \
&& mv code-server2.1698-vsc1.41.1-linux-x86_64 code-server
EXPOSE 8080 80 443
ADD run.sh /run.sh
RUN chmod 777 /run.sh
CMD ["/run.sh"]
这个版本的Dockerfile不再需要处理certbot相关的SSL/HTTPS逻辑,构建起来也轻松许多。转而在run.sh中处理一些简单逻辑:
#!/bin/bash
cd home
马赛克 这一部分从一直在线的主机鉴权并下载wildcard证书
export PASSWORD=9090980
/home/code-server/code-server --port 443 --cert /home/fullchain.pem --cert-key /home/privkey.pem
阿里云API操作
既然Docker image已经构造好了,那么只需要调用阿里云的API,把其他步骤做好就是。由于我们改进了思路,所以到这一步还需要完成的任务就是:
- 申请一个弹性公网IP x.x.x.x
- 更新二级域名code.example.com的解析到x.x.x.x
- 申请弹性容器实例,绑定弹性IP x.x.x.x,以准备好的docker image启动,绑定弹性IP x.x.x.x
可以说比较简单了,node.js实现如下:
const aliCore = require('@alicloud/pop-core')
const accessKeyId = 马赛克
const accessKeySecret = 马赛克
interface InnerModel {
ip: String,
EIPID: String,
password: String,
containerGroupId: String
}
var innerModel: InnerModel = {
ip: '',
EIPID: '',
password: '',
containerGroupId: ''
}
var clientForECI = new aliCore({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
endpoint: 'https://eci.aliyuncs.com',
apiVersion: '2018-08-08'
});
var clientForVPC = new aliCore({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
endpoint: 'https://vpc.aliyuncs.com',
apiVersion: '2016-04-28'
});
var clientForDNS = new aliCore({
accessKeyId: accessKeyId,
accessKeySecret: accessKeySecret,
endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09'
});
var requestOptionPost = {
method: 'POST'
};
function allocatedEipAddress() {
var paramsEIP = {
"RegionId": "cn-chengdu",
"Bandwidth": "50",
"InternetChargeType": "PayByTraffic"
}
return clientForVPC.request('AllocateEipAddress', paramsEIP, requestOptionPost)
}
function allocatedEipAddressNext(result: any){
console.log('IP address allocated!\t' + result.EipAddress)
innerModel.ip = result.EipAddress
innerModel.EIPID = result.AllocationId
return changeDomainRecord(result.EipAddress)
}
function changeDomainRecord(IPAddress: String){
var params = {
"RegionId": "cn-chengdu",
"RR": "zide",
"RecordId": "19276009458632704",
"Type": "A",
"Value": IPAddress
}
return clientForDNS.request('UpdateDomainRecord', params, requestOptionPost)
}
function changeDomainRecordNext(result: any){
console.log('Domain Record updated!')
return createECI(innerModel.EIPID, innerModel.password)
}
function createECI(EIPID: String, password: String) {
var paramsECI = {
"RegionId": "cn-chengdu",
"ContainerGroupName": "code-server",
"SecurityGroupId":马赛克,
"VSwitchId":马赛克,
"EipInstanceId":EIPID,
"Container.1.Image":"registry-vpc.cn-chengdu.aliyuncs.com/mayundaddy/code-server:0.4.1",
"Container.1.Name":"code-server",
"Container.1.Cpu":2,
"Container.1.Memory":4,
"InitContainer.1.EnvironmentVar.2":"PASSWORD="+password,
"Format": "JSON"
}
return clientForECI.request('CreateContainerGroup', paramsECI, requestOptionPost)
}
function main(){
allocatedEipAddress()
.then(allocatedEipAddressNext)
.then(changeDomainRecordNext)
.then((result: any) => {
innerModel.containerGroupId = result.ContainerGroupId
console.log(innerModel);
}, (ex: any) => {
console.log(ex);
})
}
main()
···
执行一下`node this.js`,很快一切都配置好了。不过域名解析还需要至多10分钟生效。这段时间如果急着用,可以通过不安全的方式访问https://IP开始使用。
# 云盘挂载
这一部分暂时还没来得及测试,等搞定了回来补充