本文最后更新于 2024年11月24日 晚上
1. 结构设计
2. 准备
- ECS 服务器 * 1
- OSS 对象存储 * 1
- 域名 * 1(若服务器在境内,域名需备案)
- 已安装的 picGo 软件(用于图片上传,可自行选择其他方式)
- 记事本(随时复制粘贴一些配置以及重要信息)
3. 配置 OSS 对象存储
- 前往对象存储 Bucket 列表 https://oss.console.aliyun.com/bucket 进入需要操作的 Bucket
- 在文件列表中新建两个目录
hexo_zeo
, image_zeo
,分别用于存放博客页面以及图片
- 前往 RAM 访问控制 - 用户: https://ram.console.aliyun.com/users
- 创建二个用户(不需要授权,后续在 Bucket 中授权,可以自行判断是否添加用户组):
- 允许读写 博客页面 的用户(供 hexo deploy 使用)
- 允许读写 图片 的用户(供 picGo 使用)
- 创建用户后,分别进入用户详情页,创建 AccessKey,记录 ID 和 key
- 配置 Bucket 授权策略(都要求 https 访问):
- 分别指定资源将 读写
hexo_zeo/*
, image_zeo/*
授权给子账号
- 指定资源将 只读(不包含ListObject)
hexo_zeo/*
, image_zeo/*
授权给 ecs 公网 ip
- OSS 资源预览必须使用 自定义域名,所以只能使用公网 ip
- 参考:(如何配置访问OSS文件时是预览行为?: https://help.aliyun.com/document_detail/600802.html?spm=a2c4g.107034.0.i3#section-4e5-f7l-jpp)
- 关于更多 阿里云 OSS 鉴权的流程和说明可以查看 OSS鉴权详解 https://help.aliyun.com/document_detail/212436.html
4. 校验 OSS 配置
打开 picGo,配置阿里云 OSS,输入 image_zeo
对应读写子账号的 ID 和 key,测试图片是否可以上传。
开启 ECS 服务器控制台,使用 wget
获取 OSS 中的资源,验证 ECS ip 授权是否正确
进入本地 hexo 目录,执行 npm install hexo-deployer-ali-oss --save
安装部署插件
配置 hexo
1 2 3 4 5 6 7
| deploy: type: ali-oss region: <您的oss 区域代码,例 oss-cn-hangzhou> accessKeyId: <您的oss accessKeyId> accessKeySecret: <您的oss accessKeySecret> bucket: <您的bucket name> remotePath: <您要部署的目录>
|
运行 hexo deploy
查看是否部署成功
前往 OSS 文件列表,核验文件是否上传成功
5. OSS 域名配置并开启 HTTPS
- 前往数字证书管理服务管理控制台https://yundun.console.aliyun.com/
- 添加免费证书(每年可以领20份免费证书)域名之后需要绑定到 bucket
- 前往对象存储 Bucket 列表 https://oss.console.aliyun.com/bucket 进入需要操作的 Bucket
- Bucket 配置 > 域名管理,绑定域名,记录自己填写的域名
- 点击绑定的域名,继续绑定证书
- 进入 Bucket 文件列表,使用刚刚绑定的域名采用 HTTPS 模式复制并访问文件临时 url,验证操作是否成功
之后 ECS 将通过这个域名访问 OSS。
6. 编写 spring boot 程序
开发工具 VSCode + Java Extension Pack + Spring Boot Extension Pack
6.1. 示例代码如下
以 博客文件 hexo server 为例
pom 文件
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
| <?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.5</version> <relativePath /> </parent> <groupId>com.cc01cc</groupId> <artifactId>blog-zeo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>blog-zeo</name> <description>cc01cc's blog</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.10.2</version> </dependency> <dependency> <groupId>com.aliyun</groupId> <artifactId>sts20150401</artifactId> <version>1.1.3</version> </dependency> </dependencies>
<build> <finalName>blog-zeo</finalName> <resources> <resource> <directory>src/main/resources</directory>
<filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.4</version> </plugin> </plugins> </build>
<profiles> <profile> <id>local</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <package.env>local</package.env> </properties> </profile> <profile> <id>ecs</id> <properties> <package.env>ecs</package.env> </properties> </profile> </profiles> </project>
|
Controller 层代码如下
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
| @RestController public class BlogController {
private static final Logger logger = LoggerFactory.getLogger(BlogController.class); @Resource private BlogService blogService;
@Value("${oss.url}") private String targetUrlPrefix; @Value("${oss.objectDir}") private String objectDir;
@RequestMapping("/{*objectName}") public ResponseEntity<byte[]> getFile(@PathVariable String objectName, HttpServletRequest request) { logger.info("objectName: " + objectName); logger.info("request: " + request); if (objectName.endsWith("/")) { objectName = objectName + "index.html"; } URL url; try { url = new URL(targetUrlPrefix + objectDir + objectName); logger.info("url: " + url); return blogService.sendHttpRequest(url, request); } catch (MalformedURLException e) { logger.error("url error: " + e.getMessage()); e.printStackTrace(); } return null; } }
|
service 层代码如下
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
| public ResponseEntity<byte[]> sendHttpRequest(URL url, HttpServletRequest request) {
logger.info("url: " + url); HttpHeaders headers = new HttpHeaders(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String headerName = headerNames.nextElement(); headers.set(headerName, request.getHeader(headerName)); }
HttpEntity<String> entity = new HttpEntity<>(headers); logger.info("entity: " + entity); ResponseEntity<byte[]> response; try { response = restTemplate.exchange(url.toURI(), HttpMethod.GET, entity, byte[].class); logger.info("response: " + response.getStatusCode()); return response; } catch (RestClientException e) { logger.error("RestClientException: " + e.getMessage()); e.printStackTrace(); } catch (URISyntaxException e) { logger.error("URISyntaxException: " + e.getMessage()); e.printStackTrace(); } return null; }
|
Application 层代码如下
1 2 3 4 5 6 7 8 9 10 11 12
| @SpringBootApplication public class BlogApplication {
public static void main(String[] args) { SpringApplication.run(BlogApplication.class, args); }
@Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
|
配置文件如下
1 2 3 4 5 6 7 8 9 10 11 12
| spring: application: name: springboot-zeo-blog mvc: pathmatch: matching-strategy: PATH_PATTERN_PARSER autoconfigure: exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration profiles: active: @package.env@
|
1 2 3 4 5 6 7 8
| server: port: 12050 logging: file: path: ./logs oss: url: http://127.0.0.1:4000
|
6.2. 打包测试
- 使用
maven package
打包 spring 应用
- 将
target/***.jar
上传到服务器
- 在服务器运行
java -jar ***.jar
等待启动成功
- 使用
wget http://127.0.0.1:12050/hexo/index.html
测试是否可以正常获取资源
6.3. 添加部署脚本
添加部署脚背,为后续自动部署做准备
在项目的根目录(不是仓库的根目录)添加 deploy.sh
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
|
APP_NAME=blog-zeo
PROG_NAME=$0 ACTION=$1 APP_START_TIMEOUT=20 APP_PORT=12050 HEALTH_CHECK_URL=http://127.0.0.1:${APP_PORT}/hexo/index.html APP_HOME=/home/admin/${APP_NAME} JAR_NAME=${APP_HOME}/target/${APP_NAME}.jar JAVA_OUT=${APP_HOME}/logs/start.log
mkdir -p ${APP_HOME} mkdir -p ${APP_HOME}/logs usage() { echo "Usage: $PROG_NAME {start|stop|restart}" exit 2 }
health_check() { exptime=0 echo "checking ${HEALTH_CHECK_URL}" while true; do status_code=$(/usr/bin/curl -L -o /dev/null --connect-timeout 5 -s -w %{http_code} ${HEALTH_CHECK_URL}) if [ "$?" != "0" ]; then echo -n -e "\rapplication not started" else echo "code is $status_code" if [ "$status_code" == "200" ]; then break fi fi sleep 1 ((exptime++))
echo -e "\rWait app to pass health check: $exptime..."
if [ $exptime -gt ${APP_START_TIMEOUT} ]; then echo 'app start failed' exit 1 fi done echo "check ${HEALTH_CHECK_URL} success" } start_application() { echo "starting java process" nohup java -jar ${JAR_NAME} >${JAVA_OUT} 2>&1 & echo "started java process" }
stop_application() { checkjavapid=$(ps -ef | grep java | grep ${APP_NAME} | grep -v grep | grep -v 'deploy.sh' | awk '{print$2}')
if [[ ! $checkjavapid ]]; then echo -e "\rno java process" return fi
echo "stop java process" times=60 for e in $(seq 60); do sleep 1 COSTTIME=$(($times - $e)) checkjavapid=$(ps -ef | grep java | grep ${APP_NAME} | grep -v grep | grep -v 'deploy.sh' | awk '{print$2}') if [[ $checkjavapid ]]; then kill -9 $checkjavapid echo -e "\r -- stopping java lasts $(expr $COSTTIME) seconds." else echo -e "\rjava process has exited" break fi done echo "" } start() { start_application health_check } stop() { stop_application } case "$ACTION" in start) start ;; stop) stop ;; restart) stop start ;; *) usage ;; esac
|
6.4. 搭建自动部署流水线
部署配置: https://help.aliyun.com/document_detail/153848.htm#section-ol3-f7c-7ho
Java 构建上传 > Java 构建命令
1 2 3
| cd blog mvn -B clean package -Dmaven.test.skip=true -Dautoconfig.skip
|
Java 构建上传 > 构建物上传 > 打包路径
1 2
| blog/target/blog-zeo.jar blog/deploy.sh
|
主机部署 > 部署脚本
1 2 3 4 5 6
| rm -rf /home/admin/blog-zeo/target rm -rf /home/admin/blog-zeo/deploy.sh mkdir -p /home/admin/blog-zeo tar zxvf /home/admin/app/package_blog-zeo.tgz -C /home/admin/blog-zeo/ mv /home/admin/blog-zeo/blog/* /home/admin/blog-zeo sh /home/admin/blog-zeo/deploy.sh restart
|
其他参考:Web应用构建配置: https://help.aliyun.com/document_detail/59293.html(我在仓库的根目录尝试配置了 release 文件,但似乎没有生效)
6.5. 验证部署
运行流水线,如无误则,部署成功(deploy.sh 会自动进行检测)
6.6. 添加钉钉机器人通知
7. ECS 域名配置
此处域名配置为临时配置,便于后续验证,验证无误后域名将指向 DCDN
- 我之前已经配置了一个指向 ECS 的域名
cc01cc.cn
- 在此基础上,我再配置一个
v01.static.cc01cc.cn
子域名,当前专门用于图片的访问
- 前往域名解析 https://dns.console.aliyun.com/
- 给子域名添加
A
AAAA
记录
- 前往数字证书管理服务管理控制台https://yundun.console.aliyun.com/
- 添加免费证书,并点击部署
- 填写并记录证书存储路径等参数
不推荐使用 Let’s Encrypt 证书,尤其是在开启 CDN 等配置的情况下 certbot 更新相对麻烦。
8. 配置 NGINX
我使用工具生成 NGINX 配置:NGINXConfig | DigitalOcean: https://www.digitalocean.com/community/tools/nginx?global.app.lang=zhCN
开源地址:digitalocean/nginxconfig.io: ⚙️ NGINX config generator on steroids 💉: https://github.com/digitalocean/nginxconfig.io
- 进入 ECS 控制台复制已有的 NGINX 配置
- 参考已有配置,在 NGINX 配置中调整或添加新的配置
8.1. no “ssl_certificate” 问题
中间遇到了个小坑,启动 nginx 服务的时候报错
1
| nginx: [emerg] no "ssl_certificate" is defined for the "listen ... ssl" directive in /etc/nginx/sites-enabled/cc01cc.cn.conf:1
|
不要运行注释 SSL 相关指令即可,如果已经运行了可以
8.1.1. 【方法一】
1 2
| sed -i -r -z 's/#?; ?#//g; s/(server \{)\n ssl off;/\1/g' /etc/nginx/sites-available/***.conf
|
8.1.2. 【方法二】手动取消注释
把 /etc/nginx/sites-enabled/***.conf
,
1
| #;#ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
|
以及其他类似注释手动取消
1
| ssl_certificate /etc/letsencrypt/live/***/fullchain.pem;
|
8.2. 检查 nginx 配置
配置完成后,访问指定域名,查看是否返回目标页面,同时检查 HTTPS 是否正常
8.2.1. nginx 配置排查
检查 nginx log, spring log
关于权限问题,可以使用以下命令,查看运行 nginx 的用户
9. 添加 DCDN
全站加速 - aliyun 文档: https://help.aliyun.com/product/64812.html
9.1. 添加 WAF 边缘防护
边缘WAF概述(新版) - aliyun 文档: https://help.aliyun.com/document_detail/404760.html
10. 搭建 cn 站自动部署流水线
1 2 3 4 5 6 7
| cnpm install
cnpm install -g hexo-cli
hexo clean
hexo deploy
|
10.1. 添加钉钉机器人通知
11. 添加云监控
云监控 - aliyun 文档: https://help.aliyun.com/product/28572.html
对 CDN, ECS, OSS 进行监控,包括可访问性,流量,带宽,请求次数等
11.1. 自动拨测
使用自动拨测工具,检测网站可访问性,以及信息完整性
12. 可以优化的地方
- OSS 默认域名,强制下载现在使用的是自定义域名解决,之后将尝试修改
Content-Type
或 Content-Disposition
的方式
- 暂时没有使用 DCDN,因为图片和其他资源使用了不同的域名,需要重新进行设计。
13. 图库 v2 测试
14. 记录
总计部署了 31 次