关于阿里云CLI

前段时间,使用了阿里云CLI工具,这是一个基于阿里云OpenApi开发的命令行界面工具。通过使用shell脚本结合CLI,能够快速创建ECS实例。当然,CLI的功能远不止于此,阿里云官网上能完成的任何操作,几乎都可以通过CLI来实现。

写在前面

阿里云CLI目前在快速迭代中,更新最快时,甚至两天推出了两个发行版本。然而,它并没有想象中那么稳定,甚至一些测试功能还残留在发行包中(SDK源码包中可以看出一些端倪)。脚本编写完成后需要经过反复测试,确保功能是稳定可用的。

至于为什么这么说,具体原由见下文

来自阿里云官方文档的介绍:阿里云CLI(Alibaba Cloud Command Line Interface)是基于OpenAPI建立的通用命令行工具,您可以借助阿里云CLI实现与阿里云产品的交互,在Shell工具中管理您的阿里云产品。

安装阿里云CLI

在 macOS 下,可以通过两种方式来安装阿里云 CLI:

  • 通过 HomeBrew 来安装(推荐)。
  • 手工安装。

通过 HomeBrew 安装

在安装之前,请确保您的 macOS 上已经安装了 HomeBrew。如果没有,请访问 HomeBrew 主页自行安装。

1
brew install aliyun-cli

手工安装

  1. 下载macOS终端安装包,二选一。

    • 官网:您可以通过此链接直接下载最新版本的阿里云CLI。
    • GitHub:在GitHub上下载您所需版本的阿里云CLI。
  2. 解压文件,获取名为aliyun的可执行文件。

    1. 执行如下命令,切换当前目录至$HOME/aliyun目录。
      1
      
      cd  $HOME/aliyun
    2. 执行如下命令,解压aliyun-cli-macosx-3.0.16-amd64.tgz文件到$HOME/aliyun目录下。
      1
      
      tar xzvf aliyun-cli-macosx-3.0.16-amd64.tgz
  3. 设置环境变量。

    说明
    完成本步骤之前,请确保 PATH 系统变量值中存在/usr/local/bin路径,否则请您根据实际情况为aliyun程序设置可用的环境变量。

    执行如下命令,将aliyun程序复制到/usr/local/bin目录中。

    1
    
    sudo cp aliyun /usr/local/bin

    如果您是root用户,请移除sudo命令。

    成功为aliyun程序设置环境变量后,您就可以直接在您的终端开始使用阿里云命令行工具了。

配置凭证

来自官方的说法:在使用阿里云CLI之前,您需要配置调用阿里云资源所需的凭证信息、地域、语言等。阿里云CLI在初次运行时会自动生成并使用default配置。您也可以创建并使用属于您的自定义配置。

配置 AK 凭证:

  • 在阿里云CLI中,AccessKey类型凭证被命名为AK,且为默认凭证类型。因此,使用该方式快速配置凭证时,可以忽略–mode选项。

  • 配置必填项:

    注意

    配置必填项中的 Region Id,必须是可用区的ID(如cn-qingdao-c);而后续使用CLI时,参数regionId必须是地域ID(如cn-qingdao)。

    阿里云官方文档描述的指定默认区域的Region Id,从字段名及说明文档看明显是地域ID,官方给的示例中写的也是地域ID。如果真的这么配置之后就会发现,当我们通过CLI做任何操作时都会报错:I/O time out

    另外,只有配置凭证时regionId需要填写为可用区ID,其他诸如使用CLI时的参数、OpenApi相关文档、及各种编程语言的SDK中,regionId均表示地域ID(cn-hangzhou、cn-shanghai等)

    PS:无意间发现的一个巨坑,配置错误会导致CLI不可用。甚至线上找阿里云技术人员花费三个小时都未能解决,最终暂时弃用CLI,改用Java版 SDK + Shell 的方式编写脚本。

    地域与可用区
    • 地域:地域指数据中心所在的地理区域,通常按照数据中心所在的城市划分。例如,华北2(北京)地域表示数据中心所在的城市是北京。
    • 可用区:可用区是指在同一地域内,电力和网络互相独立的物理区域。例如,华北2(北京)地域支持12个可用区,包括北京可用区A和北京可用区B等。同一可用区内实例之间的网络延时更小,其用户访问速度更快。

    更多信息参见地域和可用区

如下示例,配置名为akProfile的AccessKey凭证。(来自阿里云官方文档示例)

  • 交互式配置 配置命令如下:

    1
    
      aliyun configure --profile akProfile

    配置交互过程示例如下:

    1
    2
    3
    4
    5
    6
    7
    
    Configuring profile 'akProfile' in '' authenticate mode...
    Access Key Id []: AccessKey ID
    Access Key Secret []: AccessKey Secret
    Default Region Id []: cn-hangzhou
    Default Output Format [json]: json (Only support json))
    Default Language [zh|en] en:
    Saving profile[akProfile] ...Done.
  • 非交互式配置

    使用configure命令下的set子命令进行非交互式配置,配置命令如下:

    1
    2
    3
    4
    5
    6
    
    aliyun configure set
      --profile akProfile
      --mode AK
      --region cn-hangzhou
      --access-key-id AccessKeyId
      --access-key-secret AccessKeySecret
    注意
    示例中的Default Region Id []: cn-hangzhou--region cn-hangzhou写的是地域ID,这个示例是错误的,需要填写可用区ID,如cn-hangzhou-h

编写脚本

具体功能是:

  1. 根据启动模板快速创建实例并绑定登录秘钥。
  2. 实例创建后,执行一些初始化脚本,及某些预定义的软件安装。
  3. 等待实例初始化完成,获取实例绑定的公网IP。
  4. 根据预设置的登录秘钥,登录实例,从某个目录下载初始化时生成的pem文件。
  5. 将下载的pem文件安装到本机,供后续操作使用。

实例是采用自定义镜像创建的,在创建完成后会通过自定义用户数据执行某些Shell脚本,完成一些初始化操作。当初始化完成后,将在某个目录下生成一些秘钥文件及配置信息。

脚本一(创建实例,并调用脚本二):

 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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/bin/bash

set -e

cd $(dirname $0)

# 实例在几小时几分钟后过期。如果两者都不设置,默认3小时后过期,过期将自动释放实例。
EXPIRE_HOURS=${1:-3}
EXPIRE_MINUTES=${2:-0}
# 启动模板ID,这里涉及个人信息,删除了具体值。
START_TEMPLATE_ID=
# 地域ID
REGION_ID=cn-hongkong
# 创建实例后暂停几秒,再进行下一步操作。(实例初始化需要一些时间)
INIT_SLEEP_SECONDS=22

# 计时
START_TIME=$(date +%s)

ec(){
  echo "$(date +'%Y-%m-%d %H:%M:%S') ${*}"
}

ec "instance will expire in ${EXPIRE_HOURS} hours ${EXPIRE_MINUTES} minutes"
ec "Create instance starting..."
# 创建实例
CRETE_OUTPUT=$(aliyun ecs RunInstances --region ${REGION_ID} --RegionId "${REGION_ID}" --LaunchTemplateId "${START_TEMPLATE_ID}" --AutoReleaseTime `date -v+${EXPIRE_HOURS}H -v+${EXPIRE_MINUTES}M -u "+%Y-%m-%dT%H:%M:%SZ"`)

ec "Instance info: ${CRETE_OUTPUT}"

ec "Create instance executed for $(($(date +%s) - START_TIME)) seconds,completed."
t1=$(date +%s)

# 使用jq解析JSON并提取InstanceId
InstanceIdSet=$(echo "$CRETE_OUTPUT" | jq -rc '.InstanceIdSets.InstanceIdSet')

# 检查InstanceIdSet是否已设置且非空
if [ -z "$InstanceIdSet" ]; then
    ec "Error: The variable 'InstanceIdSet' is empty."
    exit 1 # 退出脚本,并返回错误码1
fi

ec "The instance is created successfully. The instance id is as follows:"
ec "$InstanceIdSet"
ec "Public ip will be obtained soon, please wait a moment."
ec "Will sleep ${INIT_SLEEP_SECONDS} seconds."
sleep ${INIT_SLEEP_SECONDS}

ec "About to get instance details..."
t2=$(date +%s)
# 获取实例详细信息
DETAIL_OUTPUT=$(aliyun ecs DescribeInstances --region ${REGION_ID} --RegionId "'${REGION_ID}'" --InstanceIds "$InstanceIdSet" --read-timeout 30)
t3=$(date +%s)
ec "Instance details:"
echo "${DETAIL_OUTPUT}"

PUBLIC_IP=$(echo "$DETAIL_OUTPUT" | jq -rc '.Instances.Instance[0].PublicIpAddress.IpAddress[0]')
ec "Instance public ip: ${PUBLIC_IP}"
# 检查PUBLIC_IP是否已设置且非空
if [ -z "$PUBLIC_IP" ]; then
    ec "Error: The variable 'PUBLIC_IP' is empty."
    exit 1 # 退出脚本,并返回错误码1
fi

ec "Get public ip spend $((t3-t2))"

# 设定重试次数
RETRY_COUNT=10
# 设定等待时间(秒)
WAIT_TIME=3
# 初始化计数器
counter=0

t4=$(date +%s)
# PUBLIC_IP 公网IP地址
while [ $counter -lt $RETRY_COUNT ]; do
    ec "Instance initialization..."
    if ./pem.sh -i ${PUBLIC_IP} -k hk -f vpnclient.mobileconfig; then
        break
    else
        let counter+=1
        sleep $WAIT_TIME
    fi
done

if [ $counter -eq $RETRY_COUNT ]; then
    ec "Error: Instance initialization time out."
    exit 1
fi

END_TIME=$(date +%s)

ec "All task completed."
echo "----------Statistics----------"
echo "Create Instance took $((t1 - START_TIME)) seconds."
echo "Obtaining the public IP took $((t3 - t2)) seconds."
echo "Polling the PEM took $((END_TIME-t4)) seconds."
echo "The instance initialization took $((END_TIME - t1)) seconds, which includes obtaining the public IP and polling the PEM file."
echo "All tasks take a total of $((END_TIME - START_TIME)) seconds."

脚本二,scp连接服务器,下载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
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#!/bin/bash

set -e

# 这里涉及个人信息,删除了值。
DOWNLOAD_FILE_NAME=

# 公钥别名与实际路径的映射
get_key_file_path() {
    case "$1" in
        hk)
          # 这里涉及个人信息,对值做了调整
            echo "/**/hk.pem"
            ;;
        zjk)
          # 这里涉及个人信息,对值做了调整
            echo "/**/zjk.pem"
            ;;
        *)
            echo ""
            ;;
    esac
}

# 默认参数
IP=""
KEY_ALIAS="hk"

# 使用说明
usage() {
    echo "Usage: $0 [-i <ip_address>] [-k <key_alias>] [-f <file_name>] | <ip_address> <key_alias> <file_name>"
    echo "Available key aliases: hk, zjk"
    exit 1
}

# 解析参数
if [ $# -eq 0 ]; then
    echo "args count is zero."
    usage
fi

# 尝试解析为传统参数
if [ $# -gt 3 ]; then
    while [ $# -gt 0 ]; do
        case "$1" in
            -i)
                IP="$2"
                shift 2
                ;;
            -k)
                KEY_ALIAS="$2"
                shift 2
                ;;
            -f)
                DOWNLOAD_FILE_NAME="$2"
                shift 2
                ;;
            *)
                echo "不支持的参数 $1"
                usage
                ;;
        esac
    done
elif [ $# -eq 3 ]; then
    # 直接输入IP和私钥别名
    IP="$1"
    KEY_ALIAS="$2"
    DOWNLOAD_FILE_NAME="$3"
elif [ $# -eq 2 ]; then
    # 直接输入IP和私钥别名
    IP="$1"
    KEY_ALIAS="$2"
elif [ $# -eq 1 ]; then
    # 直接输入IP,假设私钥别名使用默认值
    IP="$1"
else
    echo "参数个数不正确. $#"
    usage
fi

# 获取私钥文件路径
KEY_FILE=$(get_key_file_path "$KEY_ALIAS")

# 检查公钥别名有效性
if [ -z "$KEY_FILE" ]; then
    echo "Error: Unrecognized key alias '$KEY_ALIAS'."
    usage
fi

# 检查IP是否提供
if [ -z "$IP" ]; then
    echo "Error: IP address is required."
    usage
fi

# 检查私钥文件是否存在
if [ ! -f "$KEY_FILE" ]; then
    echo "Error: Key file at '$KEY_FILE' not found."
    exit 1
fi

# 禁用公钥检查
echo "Host server
          HostName ${IP}
          User root
          StrictHostKeyChecking no
          IdentityFile ${KEY_FILE}" > ~/.ssh/config

# 登录远程服务器
#ssh -i "$KEY_FILE" root@"$IP"
NEW_FILE_PATH=~/Downloads/${DOWNLOAD_FILE_NAME}
rm -f ${NEW_FILE_PATH}

echo "will execute this shell:"
# 以下四行涉及个人信息,对值做了调整
echo "scp server:/root/data/${DOWNLOAD_FILE_NAME} ${NEW_FILE_PATH}"
scp server:/root/data/${DOWNLOAD_FILE_NAME} ${NEW_FILE_PATH}
#echo scp -i "$KEY_FILE" root@"$IP":/root/data/${DOWNLOAD_FILE_NAME} ${NEW_FILE_PATH}
#scp -i "$KEY_FILE" root@"$IP":/root/data/${DOWNLOAD_FILE_NAME} ${NEW_FILE_PATH}
open ${NEW_FILE_PATH}

# MacOS以前的旧版本是可以直接通过命令安装描述文件的,近几年的版本已经不允许这么做了。
# 这里我们通过模拟手工操作,自动导入描述文件、自动打开确认安装窗口。(只能做到这了,后续步骤必须手工键入)
# AppleScript命令,用于打开系统偏好设置并导航至描述文件界面
APPLESCRIPT_COMMAND='
tell application "System Settings"
    activate
    delay 1
    tell application "System Events"
        keystroke "f" using {command down} -- 模拟按下Command+P,打开搜索
        keystroke "miaoshu" -- 输入搜索关键词
        delay 0.4
        keystroke return -- 选择搜索结果
        delay 0.4
        keystroke tab
        delay 0.4
        keystroke return
    end tell
end tell
'

# 执行AppleScript命令
osascript -e "$APPLESCRIPT_COMMAND"
rm -f ${NEW_FILE_PATH}