前段时间,使用阿里云CLI工具时遇到了一些意外情况,后续改用阿里云Java SDK重写了一遍脚本,在这里做下记录。
效果
本地执行以下指令:
将会在阿里云香港地域创建一个突发性能实例,并于1.5小时后自动释放。它是通过在.zshrc
(环境变量文件)中配置的一个函数,函数中调用本地的一个jar
文件来执行的。如下:
1
|
hk() { java -jar /Users/eddie/programs/aliyun-sdk-jar/aliyun-sdk-1.0.jar -Dexpire=${1:-3} }
|
从脚本中可以看出,如果hk
后不接参数,默认自动释放时间是3小时。
PS
相比阿里云CLI结合Shell,使用Java SDK写这种脚本,代码量要多上许多。
结构
jar包内部是通过调用阿里云sdk来实现对实例的创建操作的。代码文件结构如下:
- cc.wlizhi
- config
- ClientConfig.java: 阿里云客户端配置
- core
- ArgsProcessor.java: 参数处理
- Context.java:程序上下文类
- ecs
- DescribeInstances.java:查询实例详情
- RunInstances.java:创建实例
- task
- InstanceInfoQryTask.java:实例详情查询任务
- IpsecCreateTask.java:创建IPsec VPN
- util
- LogUtil.java:执行本地shell的输入流打印工具
- App:程序入口
依赖及打包插件配置:
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version> <!-- 使用最新稳定版 -->
<scope>provided</scope> <!-- 或者compile,取决于你的需求 -->
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.51</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>ecs20140526</artifactId>
<version>5.1.8</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-openapi</artifactId>
<version>0.3.2</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-console</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>tea-util</artifactId>
<version>0.2.21</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<shadeSourcesContent>true</shadeSourcesContent> <!-- 包含源代码 -->
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>cc.wlizhi.App</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
|
代码
config.properties
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# 阿里云的权限key和secret
accessKeyId=
accessKeySecret=
# 访问节点、地域
endpoint=ecs.cn-hongkong.aliyuncs.com
regionId=cn-hongkong
# 实例启动模板id
# 共享计算型 n1 (ecs.n1.tiny) 1 vCPU 1 GiB
launchTemplateId=lt-xxxxxxxxxxx
# 实例自动过期时间,单位:小时,浮点数,例如1.5表示1.5小时。
instanceExpireHours=3
|
ClientConfig
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
|
@Slf4j
@Getter
@Setter
public class ClientConfig {
public static final Double MIN_INSTANCE_EXPIRE_HOURS = 0.51;
public static final Double DEFAULT_INSTANCE_EXPIRE_HOURS = 3D;
private String accessKeyId;
private String accessKeySecret;
private String endpoint;
private String regionId;
private String launchTemplateId;
private Double instanceExpireHours;
@JSONField(serialize = false)
private volatile Client client;
public void setDefaultValueIfNecessary() {
if (instanceExpireHours == null) {
instanceExpireHours = DEFAULT_INSTANCE_EXPIRE_HOURS;
log.warn("警告:没有设置实例过期时间(浮点数,单位小时),将采用默认值【{}】。", DEFAULT_INSTANCE_EXPIRE_HOURS);
} else if (instanceExpireHours < MIN_INSTANCE_EXPIRE_HOURS) {
instanceExpireHours = MIN_INSTANCE_EXPIRE_HOURS;
log.warn("警告:实例过期时间(浮点数,单位小时)【{}】小于系统允许的最小值,将采用最小值【{}】。", instanceExpireHours, MIN_INSTANCE_EXPIRE_HOURS);
}
}
public Client getClient() throws Exception {
if (client != null) {
return client;
}
synchronized (this) {
if (client != null) {
return client;
}
// 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
// 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378657.html。
Config config = new Config()
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
.setAccessKeyId(accessKeyId)
// 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
.setAccessKeySecret(accessKeySecret);
// Endpoint 请参考 https://api.aliyun.com/product/Ecs
config.endpoint = endpoint;
client = new com.aliyun.ecs20140526.Client(config);
return client;
}
}
}
|
ArgsProcessor
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
|
@Getter
@Slf4j
public class ArgsProcessor {
// 实例自动释放时间,单位小时。
public static final String EXPIRE_HOURS_KEY = "-Dexpire=";
private final String[] args;
public ArgsProcessor(String[] args) {
this.args = args;
}
public Properties process() {
log.info("启动参数: " + JSON.toJSONString(args));
if (args == null) {
return new Properties();
}
for (String arg : args) {
if (Objects.equals(arg, "-h") || Objects.equals(arg, "help")
|| Objects.equals(arg, "-help") || Objects.equals(arg, "--help")) {
log.info("[-Dexpire=<number>]: (可选)设置过期时间,单位小时。如果不设置,将使用默认值{}。", EXPIRE_HOURS_KEY);
System.exit(0);
}
}
Properties props = new Properties();
InputStream is = this.getClass().getResourceAsStream("/config.properties");
try {
props.load(is);
} catch (IOException e) {
log.error(e.getMessage(), e);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
log.error("读取配置文件错误:");
log.error(e.getMessage(), e);
}
}
}
for (String arg : args) {
if (arg.startsWith(EXPIRE_HOURS_KEY)) {
try {
String val = arg.substring(EXPIRE_HOURS_KEY.length());
props.put("instanceExpireHours", val);
} catch (Exception ex) {
log.error("参数输入错误:{}", arg);
log.error(ex.getMessage(), ex);
}
}
}
return props;
}
}
|
Context
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
|
@Getter
@Slf4j
public class Context {
// 配置类
ClientConfig clientConfig = new ClientConfig();
// 创建实例
RunInstances runInstance = new RunInstances();
// 查询实例详情
DescribeInstances describeInstances = new DescribeInstances();
// 参数处理器
ArgsProcessor argsProcessor;
public Context(String[] args) {
// 处理jvm参数。
this.argsProcessor = new ArgsProcessor(args);
}
public void refresh() {
Properties properties = this.argsProcessor.process();
initClientConfig(properties);
initRunInstance();
initDescribeInstances();
}
private void initDescribeInstances() {
describeInstances.setClientConfig(clientConfig);
}
private void initRunInstance() {
runInstance.setClientConfig(clientConfig);
}
private void initClientConfig(Properties props) {
clientConfig.setAccessKeyId(props.getProperty("accessKeyId"));
clientConfig.setAccessKeySecret(props.getProperty("accessKeySecret"));
clientConfig.setEndpoint(props.getProperty("endpoint"));
clientConfig.setRegionId(props.getProperty("regionId"));
clientConfig.setLaunchTemplateId(props.getProperty("launchTemplateId"));
String instanceExpireHoursProp = props.getProperty("instanceExpireHours");
if (instanceExpireHoursProp != null) {
clientConfig.setInstanceExpireHours(Double.parseDouble(instanceExpireHoursProp));
}
clientConfig.setDefaultValueIfNecessary();
log.info("ClientConfig参数设置完成,参数值为:");
log.info(JSON.toJSONString(clientConfig, true));
}
}
|
DescribeInstances
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@Slf4j
@Getter
@Setter
public class DescribeInstances {
private ClientConfig clientConfig;
public DescribeInstancesResponse run(String instanceIds) throws Exception {
Client client = clientConfig.getClient();
DescribeInstancesRequest request = new DescribeInstancesRequest();
request.setRegionId(clientConfig.getRegionId());
request.setInstanceIds(instanceIds);
RuntimeOptions runtimeOptions = new RuntimeOptions();
return client.describeInstancesWithOptions(request, runtimeOptions);
}
}
|
RunInstances
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
|
@Slf4j
@Getter
@Setter
public class RunInstances {
private ClientConfig clientConfig;
public RunInstancesResponse run() {
try {
log.info("准备创建实例...");
log.info("实例将设置为【{}】小时后自动释放。", clientConfig.getInstanceExpireHours());
RunInstancesResponse res = doRun();
log.info("实例创建完成,详细信息:");
log.info(JSON.toJSONString(res, true));
return res;
} catch (TeaException error) {
log.info("实例创建异常");
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
log.info(error.getMessage());
// 诊断地址
log.info(error.getData().get("Recommend").toString());
com.aliyun.teautil.Common.assertAsString(error.message);
} catch (Exception _error) {
log.info("实例创建异常");
TeaException error = new TeaException(_error.getMessage(), _error);
// 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
// 错误 message
log.info(error.getMessage());
// 诊断地址
log.info(error.getData().get("Recommend").toString());
com.aliyun.teautil.Common.assertAsString(error.message);
}
return null;
}
private RunInstancesResponse doRun() throws Exception {
String releaseTime = calAutoReleaseTime();
Client client = clientConfig.getClient();
RunInstancesRequest runInstancesRequest = new RunInstancesRequest()
.setRegionId(clientConfig.getRegionId())
.setLaunchTemplateId(clientConfig.getLaunchTemplateId())
.setAutoReleaseTime(releaseTime);
log.info("run instance for parameters [ region_id: {},lauch_template_id: {},auto_release_time: {} ]",
clientConfig.getRegionId(), clientConfig.getLaunchTemplateId(), releaseTime);
RuntimeOptions runtime = new RuntimeOptions();
return client.runInstancesWithOptions(runInstancesRequest, runtime);
}
private String calAutoReleaseTime() {
long minutes = Math.round(clientConfig.getInstanceExpireHours() * 60);
// 获取当前的日期和时间,并设置为零时区(Z)
ZonedDateTime now = ZonedDateTime.now();
now = now.plusMinutes(minutes);
ZonedDateTime nowZoned = now.withZoneSameInstant(ZoneId.of("Z"));
// 定义自定义的时间格式,注意'T'和'Z'需要用单引号括起来作为文本
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
// 格式化并输出当前时间
return nowZoned.format(formatter);
}
}
|
InstanceCreateTask
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
|
@Slf4j
public class InstanceCreateTask {
private final Context context;
public InstanceCreateTask(Context context) {
this.context = context;
}
public RunInstancesResponse run() {
RunInstancesResponse runInstanceRes;
try {
runInstanceRes = runInstances(context);
} catch (Exception e) {
throw new RuntimeException(e);
}
return runInstanceRes;
}
private static RunInstancesResponse runInstances(Context context) throws Exception {
RunInstances runInstance = context.getRunInstance();
RunInstancesResponse runInstanceRes = runInstance.run();
if (runInstanceRes == null || runInstanceRes.getStatusCode() != 200) {
log.error("创建实例失败了,game over!!!");
return null;
}
return runInstanceRes;
}
}
|
InstanceInfoQryTask
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
|
@Slf4j
public class InstanceInfoQryTask {
/**
* 获取实例详情,每次调用间隔。
*/
static final int DESCRIBE_INTERVAL_MILLIS = 2500;
/**
* 获取实例详情,最多循环几次。
*/
static final int DESCRIBE_MAX_CYCLE = 10;
private final Context context;
public InstanceInfoQryTask(Context context) {
this.context = context;
}
/**
* @param instanceIds 实例id列表,这里是一个json格式字符串。
*/
public DescribeInstancesResponse run(String instanceIds) throws Exception {
DescribeInstances describeInstances = context.getDescribeInstances();
return describeInstances.run(instanceIds);
}
/**
* @param instanceIds 实例id列表,这里是一个json格式字符串。
* @param intervalMills 如果拿不到结果,间隔多久重试一次,时间单位:毫秒。
* @param maxQryCount 重试次数
*/
public DescribeInstancesResponse run(String instanceIds, int intervalMills, int maxQryCount) throws Exception {
DescribeInstancesResponse describeRes = null;
for (int i = 0; i < maxQryCount && isDescribeProcessing(describeRes); i++) {
log.info("开始查询公网IP...");
describeRes = run(instanceIds);
boolean processing = isDescribeProcessing(describeRes);
if (processing && i < maxQryCount - 1) {
log.info("没查到公网IP,将在{}毫秒后重试...", DESCRIBE_INTERVAL_MILLIS);
LockSupport.parkNanos(instanceIds, Duration.ofMillis(DESCRIBE_INTERVAL_MILLIS).toNanos());
} else if (processing && i == maxQryCount - 1) {
log.error("已经尝试查询{}次,实例信息查询不到,game over!!!", maxQryCount);
return null;
} else {
log.info("查询结果 " + instanceIds + " 详细信息:");
log.info(JSON.toJSONString(describeRes, true));
break;
}
}
return describeRes;
}
public DescribeInstancesResponse runWithRetry(String instanceIds) throws Exception {
return run(instanceIds, DESCRIBE_INTERVAL_MILLIS, DESCRIBE_MAX_CYCLE);
}
private static boolean isDescribeProcessing(DescribeInstancesResponse describeRes) {
return describeRes == null || describeRes.getStatusCode() != 200
|| describeRes.getBody() == null || describeRes.getBody().getInstances() == null
|| describeRes.getBody().getInstances().getInstance() == null
|| describeRes.getBody().getInstances().getInstance().isEmpty()
|| describeRes.getBody().getInstances().getInstance().get(0).getPublicIpAddress() == null
|| describeRes.getBody().getInstances().getInstance().get(0).getPublicIpAddress().getIpAddress() == null
|| describeRes.getBody().getInstances().getInstance().get(0).getPublicIpAddress().getIpAddress().isEmpty()
|| describeRes.getBody().getInstances().getInstance().get(0).getPublicIpAddress().getIpAddress().get(0) == null;
}
}
|
PemFileDownloadTask,代码中的常量SHELL_COMMAND是一个本地shell,详细内容参见下载pem文件
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
|
@Slf4j
public class PemFileDownloadTask {
/**
* 获取描述文件,调用间隔。
*/
static final int IPSEC_INTERVAL_MILLIS = 3000;
/**
* 获取描述文件,最多循环几次。
*/
static final int CALL_IPSEC_MAX_CYCLE = 10;
/**
* 这里的xxx.sh是本机的一个脚本,涉及个人信息,路径改掉了。
*/
static final String SHELL_COMMAND = "/xxx/xxx.sh";
static final String DEFAULT_REGION = "hk";
private final String downloadDir;
public PemFileDownloadTask(String downloadDir) {
this.downloadDir = downloadDir;
}
public String spliceShell(String ip, String region, String fileName) {
StringBuilder sb = new StringBuilder(SHELL_COMMAND)
.append(' ').append(ip).append(' ').append(region);
if (fileName != null) {
sb.append(fileName);
}
return sb.toString();
}
public void run(String ip) throws Exception {
run(ip, DEFAULT_REGION);
}
public void run(String ip, String region) throws Exception {
run(ip, region, null);
}
public void run(String ip, String region, String fileName) throws Exception {
String shell = spliceShell(ip, region, fileName);
// String shell = shellCommand + " -i " + ip + " -k " + region + " -f " + fileName;
int cycleCount = CALL_IPSEC_MAX_CYCLE;
for (int i = 0; i < cycleCount; i++) {
log.info("The shell will be execute:");
log.info(shell);
Process exec = Runtime.getRuntime().exec(shell, null, new File(downloadDir));
int exitCode = exec.waitFor();
try (InputStream is = exec.getInputStream(); InputStream es = exec.getErrorStream()) {
LogUtil.writeLog(is, es);
}
log.info("The shell [{}] return exitCode with: {}", shell, exitCode);
if (exitCode == 0) {
break;
} else if (i < cycleCount - 1) {
LockSupport.parkNanos(ip, Duration.ofMillis(IPSEC_INTERVAL_MILLIS).toNanos());
}
}
}
}
|
LogUtil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Slf4j
public class LogUtil {
public static void writeLog(InputStream... iss) throws IOException {
for (InputStream is : iss) {
String line;
if (is.available() > 0) {
BufferedReader br = new BufferedReader(new InputStreamReader(is));
while ((line = br.readLine()) != null) {
log.info(line);
}
}
}
}
}
|
App
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
|
@Slf4j
public class App {
public static void main(String[] args) throws Exception {
long t1 = System.currentTimeMillis();
Context context = new Context(args);
context.refresh();
InstanceCreateTask instanceCreateTask = new InstanceCreateTask(context);
RunInstancesResponse runInstanceRes = instanceCreateTask.run();
if (runInstanceRes == null) return;
List<String> instanceIdSet = runInstanceRes.getBody().getInstanceIdSets().getInstanceIdSet();
String instanceIds = JSON.toJSONString(instanceIdSet, true);
log.info("instanceIdSet: " + instanceIds);
long t2 = System.currentTimeMillis();
long delayMillis = 22000;
log.info("实例初始化中,将在{}毫秒后获取公网IP、下载密钥文件。", delayMillis);
Thread.sleep(delayMillis);
InstanceInfoQryTask instanceInfoQryTask = new InstanceInfoQryTask(context);
DescribeInstancesResponse describeRes = instanceInfoQryTask.runWithRetry(instanceIds);
if (describeRes == null) return;
String ip = describeRes.getBody().getInstances().getInstance().get(0).getPublicIpAddress().getIpAddress().get(0);
log.info("成功查询到公网IP:" + ip);
PemFileDownloadTask downloadTask = new PemFileDownloadTask("/Users/eddie/Desktop");
downloadTask.run(ip);
long t3 = System.currentTimeMillis();
log.info("----------执行耗时统计----------");
log.info("创建实例耗时 {}ms", t2 - t1);
log.info("实例初始化及秘钥下载耗时 {}ms", t3 - t2);
log.info("任务执行总耗时 {}ms", t3 - t1);
log.info("Execution complete.");
}
}
|