阿里云Java SDK创建ECS实例

前段时间,使用阿里云CLI工具时遇到了一些意外情况,后续改用阿里云Java SDK重写了一遍脚本,在这里做下记录。

效果

本地执行以下指令:

1
hk 1.5

将会在阿里云香港地域创建一个突发性能实例,并于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.");
    }
}