如何通过阿里云 OpenAPI 管理 CDN

前言

前不久,阿里云通知笔者 CDN 的 SSL 证书快到期了,于是笔者就打算新申请一张 1 年的 SSL 证书。

【变更】关于免费证书服务策略调整通知

对于免费领取的 SSL 证书,签发后的证书有效期统一变更为 3 个月

结果发现,现在免费的证书只有 3 个月了,跟 Let’s Encrypt 提供的 90 天免费证书一样了。

既然如此,为什么不直接使用 Let’s Encrypt 提供的免费证书呢?

在之前,由于 Let’s Encrypt 只提供最长 90 天的免费证书,为了省点事,选择了阿里云的 1 年期免费证书。

关于 Let’s Encrypt 为什么只提供 90 天的免费证书,详见:Why ninety-day lifetimes for certificates?

image-20240609202714928

最重要的是 Let’s Encrypt 完全免费!

经过一通研究,笔者发现可以通过调用阿里云 OpenAPI 来实现自动化更新 SSL 证书,在此记录一下使用方式。

什么是阿里云 OpenAPI

OpenAPI 是指开放应用程序编程接口(Open Application Programming Interface)的简称。阿里云 OpenAPI 为开发者提供了一系列开放的应用程序接口,使您可以通过这些应用程序接口来方便的管理云上资源、数据和服务等内容。

详见:什么是 OpenAPI

总之,我们可以通过阿里云提供的 OpenAPI,来调用阿里云上的一系列接口,从而管理对应的资源和服务。

获取 AccessKey

在一切开始之前,需要先获取到 AccessKeyId 和 AccessKeySecret。

如果你现在点击 左上角的 AccessKey 管理的话,就会发现,阿里云提示你使用 子用户 AccessKey

image-20240609204527843

image-20240609204621782

在这里,出于安全起见,十分建议使用子用户 AccessKey

可以前往 子用户管理 页面进行创建。

创建用户的时候记得选上 OpenAPI 调用访问

image-20240609204949690

详见:创建 RAM 用户

记得及时保存 AccessKey IDAccessKey Secret,关掉之后就看不见了。

image-20240609205501826

创建完子用户后还需要给子用户授权。

由于目前只需要管理 CDN,因此这里只给子用户授权 CDN 相关的权限即可。

因为还需要写入域名证书,故需要拿到写入权限,不要选择 只读 权限。

image-20240609205048058

如何使用 OpenAPI 管理 CDN

在调用 OpenAPI 之前,显然需要先知道这个 API 是干什么的,有哪些入参,返回值又是什么样的。

查看 API 文档

所幸,阿里云提供了完善的 API 文档。

详见:阿里云 API 文档

经过一番搜索,就可以看到需要的 API 了。

DescribeDomainCertificateInfo - 查询域名证书信息SetCdnDomainSSLCertificate - 设置CDN域名证书。一个用于查看当前域名的证书信息,另一个用于设置证书。

image-20240609205819013

详见:DescribeDomainCertificateInfo- 查询域名证书信息

SetCdnDomainSSLCertificate- 设置 CDN 域名证书

查看 API 代码调用示例

点击右上方的 去调用 即可调试相关接口。

参考:DescribeDomainCertificateInfo

在输入输入参数后,即可点击左侧的 SDK 示例查看具体的代码。

image-20240609205944808

由于笔者比较喜欢用 TypeScript,所以此处的例子是用 TypeScript 实现的。

读者朋友也可以使用自己喜欢的编程语言进行调用。

image-20240609210238588

阿里云提供了包括 Java、TypeScript、Go、PHP、Python 等多种语言的 SDK,可以选一个喜欢的进行使用。

基于同样的原理,也可以获取到 SetCdnDomainSSLCertificate 的代码逻辑。

封装 API 调用

参考阿里云给出的例子后,稍加改动,就得到了以下代码。

import Cdn20180510, * as $Cdn20180510 from '@alicloud/cdn20180510'
import * as $OpenApi from '@alicloud/openapi-client'
import * as $Util from '@alicloud/tea-util'

export default class Client {

    /**
   * 使用AK&SK初始化账号Client
   * @return Client
   * @throws Exception
   */
    static createClient(): Cdn20180510 {
        // 工程代码泄露可能会导致 AccessKey 泄露,并威胁账号下所有资源的安全性。以下代码示例仅供参考。
        // 建议使用更安全的 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378664.html。
        const config = new $OpenApi.Config({
            // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_ID。
            accessKeyId: process.env['ALIBABA_CLOUD_ACCESS_KEY_ID'],
            // 必填,请确保代码运行环境设置了环境变量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。
            accessKeySecret: process.env['ALIBABA_CLOUD_ACCESS_KEY_SECRET'],
        })
        // Endpoint 请参考 https://api.aliyun.com/product/Cdn
        config.endpoint = 'cdn.aliyuncs.com'
        return new Cdn20180510(config)
    }

    // 查询域名的证书
    static async describeDomainCertificateInfoWithOptions(domainName: string) {
        const client = Client.createClient()
        const describeDomainCertificateInfoRequest = new $Cdn20180510.DescribeDomainCertificateInfoRequest({
            domainName,
        })
        const runtime = new $Util.RuntimeOptions({})
        try {
            // 复制代码运行请自行打印 API 的返回值
            return (await client.describeDomainCertificateInfoWithOptions(describeDomainCertificateInfoRequest, runtime)).body
        } catch (error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            // 错误 message
            console.error(error?.message)
            // 诊断地址
            console.error(error?.data?.['Recommend'])

        }
    }
    /**
     * 设置域名证书
     *
     * @author CaoMeiYouRen
     * @date 2024-05-22
     * @static
     * @param domainName 域名
     * @param certName 证书名称
     * @param SSLPub 证书内容
     * @param SSLPri 私钥内容
     */
    static async setCdnDomainSSLCertificateWithOptions(domainName: string, certName: string, SSLPub: string, SSLPri: string) {
        const client = Client.createClient()
        const setCdnDomainSSLCertificateRequest = new $Cdn20180510.SetCdnDomainSSLCertificateRequest({
            domainName,
            certName,
            certType: 'upload',
            SSLProtocol: 'on',
            SSLPub,
            SSLPri,
        })
        const runtime = new $Util.RuntimeOptions({})
        try {
            // 复制代码运行请自行打印 API 的返回值
            return (await client.setCdnDomainSSLCertificateWithOptions(setCdnDomainSSLCertificateRequest, runtime)).body
        } catch (error) {
            // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。
            console.error(error?.message)
            // 诊断地址
            console.error(error?.data?.['Recommend'])
        }
    }

}

这里通过设置 ALIBABA_CLOUD_ACCESS_KEY_IDALIBABA_CLOUD_ACCESS_KEY_SECRET 两个环境变量来进行调用 API,相对安全。

如果需要更安全的方式,可以考虑通过 STS 方式,更多鉴权访问方式请参见:https://help.aliyun.com/document_detail/378664.html

实现自动更新 SSL 证书逻辑

最后,再添加一些判断逻辑进去,就是实现了自动更新 SSL 证书,这里是在过期时间小于 31 天的时候就更新证书。

import path from 'path'
import dotenv from 'dotenv'
import dayjs from 'dayjs'
import fs from 'fs-extra'
import CdnClient from './apis/cdn' // './apis/cdn' 文件就是上面的示例。

dotenv.config({ path: ['.env.local', '.env'] })

// 更新 CDN 证书
async function updateCdnCertificate(domainName: string) {
    // 域名证书位置
    const CDN_SSL_DIR_PATH = process.env.CDN_SSL_DIR_PATH
    const certificateInfo = await CdnClient.describeDomainCertificateInfoWithOptions(domainName)

    const days = dayjs(certificateInfo.certInfos.certInfo[0].certExpireTime).diff(dayjs(), 'days')
    console.log(`域名 NULL 还有 NULL 天过期`)

    if (days < 31) { // 如果小于 31 天,更新证书
        console.log(`域名 NULL 的证书即将过期,正在更新中`)
        const certName = `NULL-${dayjs().format('YYYY-MM-DD')}`
        // 此处假定是 Let’s Encrypt 提供的域名证书,公钥是 fullchain.pem,私钥是 privkey.pem。其他类型的证书也类似
        const SSLPub = await fs.readFile(path.join(CDN_SSL_DIR_PATH, 'fullchain.pem'), 'utf-8')
        const SSLPri = await fs.readFile(path.join(CDN_SSL_DIR_PATH, 'privkey.pem'), 'utf-8')
        const certificateResponseBody = await CdnClient.setCdnDomainSSLCertificateWithOptions(domainName, certName, SSLPub, SSLPri)
        console.log(JSON.stringify(certificateResponseBody))
        console.log
        return
    }
    console.log(`域名 NULL 的证书还未过期,跳过更新`)
}

updateDcdnCertificate('example.com')

最后,把这份代码设置为定时任务,每天执行一遍,即可实现自动更新 SSL 证书!

总结

除了更新 CDN 证书外,阿里云 OpenAPI 自然也可以更新 DCDN 域名证书、管理 DNS 解析、管理图床、管理 WAF 等。

总之,只要是阿里云上提供的服务,基本上都能通过阿里云 OpenAPI 来管理。对自动化来说,是极大的利好。

不过,需要注意的是,也要保管好 AccessKey IDAccessKey Secret,以免泄露,从而造成损失。