更新配置自动化工具

缘起

  我们小创业公司使用的宽带是个人家庭宽带(申请企业宽带的成本很高,每月几千块),公网IP过一段时间就会变动,平时使用都没有什么影响,只有一点,阿里云上的服务都配置了安全组,只允许阿里云内网或者白名单IP访问,从而保障公司服务安全性。公司公网IP会动态变更,每一次变更之后都要修改安全组的ip配置,十分麻烦。

  来自攻城师的思考:每一次IP变动都打断了大家专心工作的心流(Flow)状态,而且登录阿里云控制台修改也很繁琐,修改安全组配置的IP名单,能不能自动化?

  大概瞄了一下阿里云相关的API,这事情可以搞。

动手实现

看看需要实现哪些功能:

  • 定时调度
  • 获取公司宽带的公网IP地址
  • 调用阿里云API刷新安全组的白名单IP

如何获取公司宽带的公网IP地址?

这个简单,市场上有很多ip查询的网站,比如 ip.cn,ipip.net ,你访问他们的网站,默认就会显示你的公网ip。

调用阿里云API没什么难度,只是工作量。

核心逻辑就是,获取当前公司ip,存储下来,每隔N分钟,再次获取公司当前ip,与之前记录对比,如果不同,则调用阿里云api,更新安全组白名单IP,最后存储新IP。否则什么也不做。

于是我创建了一个SpringBoot应用,为什么要创建一个java web应用?首先因为最熟悉java,其次是工程创建快速,使用简单,之前我实现了一个模版工程,可以快速创建SpringBoot应用,而且配置了常用的类库,非常方便。

一些核心功能实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Component
public class ScheduleComp {

Logger logger = LoggerFactory.getLogger(ScheduleComp.class);

@Autowired
RefreshSecurityRulService securityRulService;

/**
* 每隔3分钟检查一下
*/
@Scheduled(cron = "0 */3 * * * ?")
public void cornJob() {
logger.warn(" >>cron执行....");
securityRulService.refreshAll(false);
}
}

securityRulService 的refreshAll()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void refreshAll(boolean force) {
String nowIp = getLocalIp();
if (StringUtils.isEmpty(nowIp)) {
logger.error("get current ip is empty! try next time...");
return;
}
String oldIp = getCurrentIp();
if (StringUtils.isEmpty(oldIp)) {
throw new RuntimeException("current ip data is empty!");
}
if (StringUtils.equals(nowIp, oldIp) && !force) {
logger.warn("same ip, nothing todo");
return;
}
ecsRefreshSecurityRuleService.refreshAll(nowIp, oldIp);

updateCurrentIp(nowIp);
}

getLocalIp()的实现:

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
public static String getLocalIp() {
String ip = getIpCN();
...

return ip;
}

public static String getIpCN() {
//<p>您现在的 IP:<code>114.0.1.2</code>
return getLocalIp("http://ip.cn", "<p>您现在的 IP:<code>");
}


public static String getLocalIp(String url, String prefix) {
Request request = new Request.Builder()
.get()
.url(url)
.build();
try {
Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
String regexPatten = prefix + "((?:(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d))).){3}(?:25[0-5]|2[0-4]\\d|((1\\d{2})|([1-9]?\\d))))";
Pattern pattern = Pattern.compile(regexPatten);
String resp = response.body().string();
Matcher matcher = pattern.matcher(resp);
if (matcher.find()) {
String matchStr = matcher.group(0);
return matchStr.replace(prefix, "");
}
}
response.close();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}

这里通过 ip.cn 获取本机的公网地址。

还可以配置其他网站,防止ip.cn挂掉而导致服务失败。

getCurrentIp 的实现很简单,从本地文件读取上次记录的ip地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public String getCurrentIp() {
File file = new File("ip.txt");
if (!file.exists()) {
throw new RuntimeException("cannot find ip data!");
}
try {
BufferedReader bufferedReader =
new BufferedReader(new InputStreamReader(new FileInputStream(file)));
String strLine = bufferedReader.readLine();
bufferedReader.close();
return strLine.trim();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}

采用文件存储是最简单直接的方式。更新文件内容的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void updateCurrentIp(String ip) {
if (StringUtils.isEmpty(ip))
return;
File file = new File("ip.txt");
file.delete();
try {
file.createNewFile();
BufferedWriter bufferedWriter =
new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
bufferedWriter.write(ip.trim());
bufferedWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
}

刷新阿里云安全组配置的逻辑:

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
69
70
71
public void refreshAll(String nowIp, String oldIp) {

List<SecurityRuleDO> newSecurityRuleDOList = new ArrayList<>(testServer(nowIp));
for (SecurityRuleDO securityRuleDO : newSecurityRuleDOList) {
refresh(securityRuleDO);

}

if (!StringUtils.equals(nowIp, oldIp)) {
List<SecurityRuleDO> oldSecurityRuleDOList = new ArrayList<>();
oldSecurityRuleDOList.addAll(testServer(oldIp));
for (SecurityRuleDO securityRuleDO : oldSecurityRuleDOList) {
deleteOld(securityRuleDO);
}

}
}

private List<SecurityRuleDO> testServer(String ip) {
List<SecurityRuleDO> securityRuleDOList = new ArrayList<>();
SecurityRuleDO securityRuleDO = new SecurityRuleDO();
securityRuleDO.sourceIp = ip;
securityRuleDO.groupId = "YourECSGroupId";
securityRuleDO.ipProtocol = "tcp";
securityRuleDO.portRange = "yourPort/yourPort";
securityRuleDOList.add(securityRuleDO);
...
return securityRuleDOList;
}


public boolean refresh(SecurityRuleDO securityRuleDO) {
AuthorizeSecurityGroupRequest request =
new AuthorizeSecurityGroupRequest();
request.setSecurityGroupId(securityRuleDO.groupId);
request.setIpProtocol(securityRuleDO.ipProtocol);
request.setPortRange(securityRuleDO.portRange);
request.setNicType(securityRuleDO.nicType);
request.setPolicy("accept");
request.setSourceCidrIp(securityRuleDO.sourceIp);

try {
AuthorizeSecurityGroupResponse response = client.getAcsResponse(request);
String requestId = response.getRequestId();
System.out.println(requestId);
return true;
} catch (ClientException e) {
e.printStackTrace();
return false;
}
}

public boolean deleteOld(SecurityRuleDO securityRuleDO) {
RevokeSecurityGroupRequest request = new RevokeSecurityGroupRequest();
request.setSecurityGroupId(securityRuleDO.groupId);
request.setIpProtocol(securityRuleDO.ipProtocol);
request.setPortRange(securityRuleDO.portRange);
request.setNicType(securityRuleDO.nicType);
request.setPolicy("accept");
request.setSourceCidrIp(securityRuleDO.sourceIp);

try {
RevokeSecurityGroupResponse response = client.getAcsResponse(request);
String requestId = response.getRequestId();
System.out.println(requestId);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

其中client的初始化:

1
2
DefaultProfile profile = DefaultProfile.getProfile(reginId, appkey, secret);
client = new DefaultAcsClient(profile);

小问题

定时3分钟刷新有个小问题,就是有极小的概率,发生IP变更但是还没有及时刷新安全组的配置。怎么办,再加一个主动刷新的接口呗:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
public class RefreshSecurityRuleController {
@Autowired
RefreshSecurityRulService securityRulService;

@GetMapping("/ip")
public String getMyIp() {
return securityRulService.getLocalIp();
}

@GetMapping("/fresh")
public String forceFresh() {
securityRulService.refreshAll(true);
return "done!";
}

}

其中的/fresh强制刷新IP。还附赠了一个接口,获取公司当前的IP。

这个小应用部署在公司局域网内,一直工作的很好,再也不用烦心IP变动这些琐事。

直到有一次,ip.cn 网站挂了。挂了很久,我们就增加了从ip.tool.chinaz.com 网站获取IP。

收工。