按需分配随时可用的在线开发环境:弹性容器+code-server踩坑记

需求整理

微软在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开始使用。

# 云盘挂载
这一部分暂时还没来得及测试,等搞定了回来补充
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容