Java BufferedImage OutOfMemoryError

最近遇到了BufferedImage OutOfMemoryError的问题,在此记录一下。

事情的起因是项目中有个功能需要将多张图片合并为一张,流程如下:

  1. 有三个图片文件,File A,File B,File C
  2. 通过ImageIO.read(inputStream) 将ABC转换成BufferedImage类型
  3. 通过java.awt.image.BufferedImage#getWidth()获取这三张图片中的最大宽度maxWidth
  4. 计算另外两张图片等比例拉伸至maxWidth后的高度,将这三张图片的高度相加得到maxHeight
  5. 创建一张maxWidth * maxHeight的图片
  6. 通过画布将三张图片写入到这张图片里

主要代码如下:

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
...
val images = mutableListOf<BufferedImage>()
...
images.add(ImageIO.read(inputStream))
...
joinImages(*images.toTypedArray())
...

fun joinImages(vararg imgs: BufferedImage): BufferedImage {
val offset = 20

val aggregateWidth = imgs.maxBy { it.width }!!.width
val aggregateHeight = imgs.sumBy {
(it.height * aggregateWidth) / it.width
} + (imgs.size - 1) * offset

val newImage = BufferedImage(aggregateWidth, aggregateHeight, BufferedImage.TYPE_INT_ARGB)
val g2 = newImage.createGraphics()
val oldColor = g2.color

g2.paint = Color.white
g2.fillRect(0, 0, aggregateWidth, aggregateHeight)
g2.color = oldColor

var y = 0

imgs.forEach {
val height = (aggregateWidth * it.height) / it.width
val scaled = it.getScaledInstance(aggregateWidth, height, Image.SCALE_DEFAULT)

g2.drawImage(toBufferedImage(scaled), null, 0, y)
y += height + offset
}
g2.dispose()

return newImage
}

运行过程中发现第417行代码时不时会抛出OutOfMemoryError错误,研究了下发现有两个问题:

  1. 对于jpeg格式的图片,java将其载入到内存时是不会对其进行压缩的,一个像素会占用3个字节的内存,如果图片的尺寸比较大,会占用非常大的内存,比如这张图片,实际文件大小为84kb,载入到内存里后的大小为11mb:
1
2
3
4
5
6
val file = File("/Downloads/fff.jpeg")
val image = ImageIO.read(FileInputStream(file))

println(image.width) //2000
println(image.height) //2000
println(ObjectSizeCalculator.getObjectSize(image)) //12000928 -> 11mb
  1. 合并图片前代码将这三张图片一次性全部加载到内存里,这也会占用比较大的内存。

对于问题1,目前只能规避这问题,加载图片前会预先判断下该图片会占用多少内存,对于会超出内存使用的jpeg图片不予合并。同时在代码第14行,我们对合并后的最大宽度进行限制,避免合并后的图片尺寸过大,占用的内存超出限制。

对于问题2,这三张图片可以按序加载,不用一次性全部加载到内存里,在需要合并时才加载到内存里,同时可以改进获取图片尺寸的代码,不用将图片加载到内存后再获取尺寸。

最终代码如下:

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
fun getImageSize(file: File): Dimension {
ImageIO.createImageInputStream(file).use { `in` ->
val readers = ImageIO.getImageReaders(`in`)
if (readers.hasNext()) {
val reader = readers.next()
try {
reader.input = `in`
return Dimension(reader.getWidth(0), reader.getHeight(0))
} finally {
reader.dispose()
}
}
}

return Dimension(0, 0)
}


fun joinImages(vararg files: File): File {
...
val offset = 20
val dimensions = files.map { getImageSize(it) }
val aggregateWidth = min(dimensions.maxBy { it.width }!!.width, 800)
val aggregateHeight = dimensions.sumBy { (it.height * aggregateWidth) / it.width } + (files.size - 1) * offset
val newImage = try {
BufferedImage(aggregateWidth, aggregateHeight, BufferedImage.TYPE_INT_ARGB)
} catch (e: OutOfMemoryError) {
...
throw e
}
val g2 = newImage.createGraphics()
val oldColor = g2.color

g2.paint = Color.white
g2.fillRect(0, 0, aggregateWidth, aggregateHeight)
g2.color = oldColor

var y = 0

files.forEach {
var image = try {
ImageIO.read(it)
} catch (e: OutOfMemoryError) {
...
throw e
}
val height = (aggregateWidth * image.height) / image.width
val scaled = image.getScaledInstance(aggregateWidth, height, Image.SCALE_DEFAULT)

g2.drawImage(toBufferedImage(scaled), null, 0, y)

image.flush()
image = null

y += height + offset
}
g2.dispose()

val joinImageFile = File.createTempFile(UUID.randomUUID().toString(), ".png")

ImageIO.write(newImage, "png", joinImageFile)

return joinImageFile
}

按上述代码修改后,再也没有发生OutOfMemoryError。对于问题1,目前发现apache commons-imaging似乎可以解决这问题,有时间去尝试下,到时候再来更新本文。

参考:

https://coderanch.com/t/416485/java/Java-BufferedImage-OutOfMemoryError

Kubernetes环境配置JVM内存

我们知道JVM在docker容器环境中是无法正确检测到可用内存的,最近正好遇到了一个与之相关的问题,在此记录一下。

遇到问题的项目技术栈为JDK 8 + Spring Boot + Tomcat,部署在docker环境。项目运行过程中出现了java.lang.OutOfMemoryError: Java heap space异常,当时项目的部署文件如下:

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
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: api-deployment
labels:
app: api
spec:
serviceName: api-app
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
terminationGracePeriodSeconds: 30
containers:
- image: ...
imagePullPolicy: "Always"
name: api
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 300
periodSeconds: 5
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 60
periodSeconds: 5
securityContext:
capabilities:
add:
- SYS_PTRACE
envFrom:
- secretRef:
name: secret

问题应该出在k8s内存设置与JVM的配置这边,网上查询资料后发现tomcat可以通过CATALINA_OPTS环境变量来设置JVM参数,UseCGroupMemoryLimitForHeap 可以让JVM自动检测容器的可用内存,MaxRAMFraction 为容器内存和堆内存的比例,比如容器内存为2G,MaxRAMFraction为2,则最大堆内存为2G/2=1G,这里将MaxRAMFraction设置为2比较安全,设置了这两个参数后,JVM就能通过检测容器的内存来自动调整堆内存大小,不用再显示设置堆内存了。

更新后的配置文件里加了如下代码:

1
2
3
4
5
6
7
...
env:
- name: CATALINA_OPTS
value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2"
resources:
requests:
memory: "512Mi"

项目运行一段时间后发现问题依旧,研究了下UseCGroupMemoryLimitForHeap参数,发现它是通过读取系统/sys/fs/cgroup/memory/memory.limit_in_bytes文件来检测内存的,登录到容器里查看了下该文件,发现里面是一个很大的值:9223372036854771712,等于没有内存限制,查了下资料发现这个字段是通过k8s文件中的resources->limits的这个属性来配置的,更新文件,加了如下代码:

1
2
limits:
memory: "2048Mi"

观察一段时间后内存就没再溢出,最终完整配置文件如下:

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
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: api-deployment
labels:
app: api
spec:
serviceName: api-app
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
terminationGracePeriodSeconds: 30
containers:
- image: ...
imagePullPolicy: "Always"
name: api
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 300
periodSeconds: 5
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 60
periodSeconds: 5
securityContext:
capabilities:
add:
- SYS_PTRACE
env:
- name: CATALINA_OPTS
value: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2"
envFrom:
- secretRef:
name: secret
resources:
requests:
memory: "512Mi"
limits:
memory: "2048Mi"

参考

Google Cloud Pub Sub的一些研究

  • 多个客户端可以使用一个subscription,每个客户端只会收到一部分的消息:

Multiple subscribers can make pull calls to the same “shared” subscription. Each subscriber will receive a subset of the messages
出处:https://cloud.google.com/pubsub/docs/subscriber#push-subscription

  • 对于一些处理比较耗时的消息,客户端有后台任务会自动更新ack
    deadline,详情可见extendDeadlines方法。
  • 关于Message retention duration

By default, a message that cannot be delivered within the maximum retention time of 7 days is deleted and is no longer accessible. This typically happens when subscribers do not keep up with the flow of messages. Note that you can configure message retention duration (the range is from 10 minutes to 7 days).

意思就是如果消息在Message retention duration时间内没有被处理完成的话,这条消息就会删除并且再也访问不到。

比如程序的BUG导致消息一直无法被处理,经过 Message retention duration 后这条消息就再也收不到了,会导致数据丢失。

Safari can't establish a secure connection 解决办法

公司测试用的是6.x版本的Safari,在测试时发现无法打开项目网站,提示Safari can't establish a secure connection错误,测试了下其他网站也只有一部分能打开。

一开始以为是系统配置的问题,尝试了下网上的解决方案如:修改DNS服务器地址,信任证书等,发现还是无法打开项目网站。后来发现Safari 6.x是不支持TLS 1.2版本的,而我们项目用的正好是1.2版本的TLS。

解决办法:

  1. 升级Safari版本
  2. 让项目支持低版本的TLS

参考:

Google Cloud Function Http 认证配置

最近在做的项目用到了google cloud,其中有个功能模块的google cloud function需要调用另一个通过http请求触发的function,在研究配置http function认证信息期间花了不少时间,在这记录一下。

默认情况下创建http请求触发的function会勾选Allow unauthenticated invocations选项,这样无需通过认证就可以调用function,但这样做显然是不安全的,一旦接口地址泄漏就可能会被恶意调用。

查看了google cloud文档,google建议用Cloud Endpoint来做认证,但是在cloud console始终无法创建endpoint,遂放弃。

一开始想了个临时的解决方案:在勾选Allow unauthenticated invocations的情况下,在function代码里加上token验证,如果token不匹配就返回并提示forbidden,虽然可以减少function在被恶意调用的情况下的执行时间,但还是会产生一定的费用。

今天抽时间重新研究了下这个问题,终于找到了解决方案

假设调用者function为 xxx-master,被调用的function为xxx-slave,

  • 通过下面命令给 xxx-slave function加上roles/cloudfunctions.invoker 角色,允许 roles/cloudfunctions.invoker这个角色来调用xxx-slave function
1
2
3
gcloud beta functions add-iam-policy-binding RECEIVING_FUNCTION \
--member='serviceAccount:CALLING_FUNCTION_IDENTITY' \
--role='roles/cloudfunctions.invoker'

RECEIVING_FUNCTION -> 被调用的function名字
CALLING_FUNCTION_IDENTITY -> 一般为 PROJECT_ID@appspot.gserviceaccount.com

比如:

1
2
3
gcloud beta functions add-iam-policy-binding xxx-slave \
--member='serviceAccount:YOUR_PROJECT_ID@appspot.gserviceaccount.com' \
--role='roles/cloudfunctions.invoker'
  • xxx-master function中加上获取token代码,请求时将token放在Authorization header里,以python为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Requests is already installed, no need to add it to requirements.txt
import requests

def calling_function(request):
# Make sure to replace variables with appropriate values
receiving_function_url = 'https://us-central1-graphical-bus-248617.cloudfunctions.net/xxx-slave
'

# Set up metadata server request
# See https://cloud.google.com/compute/docs/instances/verifying-instance-identity#request_signature
metadata_server_token_url = 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience='

token_request_url = metadata_server_token_url + receiving_function_url
token_request_headers = {'Metadata-Flavor': 'Google'}

# Fetch the token
token_response = requests.get(token_request_url, headers=token_request_headers)
jwt = token_response.content.decode("utf-8")

# Provide the token in the request to the receiving function
receiving_function_headers = {'Authorization': f'bearer {jwt}'}
function_response = requests.get(receiving_function_url, headers=receiving_function_headers)

return function_response.content

完成上面两部后,xxx-slave function就可以在被认证的情况下调用了。

这里还需要注意的是要将Cloud Functions Invoker 中的 all user移除,不然xxx-slave方法还是公开的,操作步骤:

  • Google Cloud Console -> Cloud Function
  • 勾选xxx-slave function
  • 点击左侧的 PERMISSIONS tab
  • 点开 Cloud Functions Invoker
  • all user移除

参考:

Google Cloud使用kubernetes,let’s encrypt和nginx-ingress部署web app

环境配置

连接cluster

1
gcloud container clusters get-credentials CLUSER_NAME --zone us-central1-a --project PROJECT_NAME

安装helm和tiller

1
2
3
kubectl create serviceaccount --namespace kube-system tiller
kubectl create clusterrolebinding tiller-cluster-rule --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
helm init --service-account tiller

安装nginx-ingress

1
helm install --name nginx-ingress stable/nginx-ingress --set rbac.create=true --set controller.publishService.enabled=true

安装let’s encrypt

1
2
3
4
5
6
kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.8/deploy/manifests/00-crds.yaml

helm repo add jetstack https://charts.jetstack.io
helm install --name cert-manager --namespace cert-manager jetstack/cert-manager

kubectl create -f issuer.yaml

issuer.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
# The ACME server URL
server: https://acme-v02.api.letsencrypt.org/directory
# Email address used for ACME registration
email: youremail@yourdomain.com
# Name of a secret used to store the ACME account private key
privateKeySecretRef:
name: letsencrypt-prod
# Enable the HTTP-01 challenge provider
http01: {}

配置DNS

获取nginx-ingress-controller的IP

1
kubectl get svc -n default

output:

1
2
3
4
NAME                            TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
kubernetes ClusterIP 10.64.0.1 <none> 443/TCP 64m
nginx-ingress-controller LoadBalancer 10.64.11.170 35.188.93.188 80:30393/TCP,443:30515/TCP 59m
nginx-ingress-default-backend ClusterIP 10.64.5.162 <none> 80/TCP 59m

将你的DNS记录指向 EXTERNAL-IP -> 35.188.93.188

部署web项目

创建namespace

1
2
kubectl create ns demo
kubectl config set-context $(kubectl config current-context) --namespace=demo

部署项目

Deployment

1
kubectl apply -f deployment.yaml

deplyment.yaml

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
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: api-deployment
labels:
app: api
spec:
serviceName: api-app
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
terminationGracePeriodSeconds: 30
containers:
- image: tomcat:8.5.45-jdk8-openjdk-slim
imagePullPolicy: "Always"
name: api
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 150
periodSeconds: 5
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 150
periodSeconds: 5

Service

1
kubectl apply -f service.yaml

service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: api-service
labels:
app: api
spec:
type: ClusterIP
selector:
app: api
ports:
- protocol: TCP
port: 80
targetPort: 8080

Ingress

1
kubectl apply -f ingress.yaml

ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: name-virtual-host-ingress
annotations:
kubernetes.io/ingress.class: nginx
certmanager.k8s.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls: # < placing a host in the TLS config will indicate a cert should be created
- hosts:
- demo.w2x.me
secretName: letsencrypt-prod
rules:

- host: demo.w2x.me
http:
paths:
- backend:
serviceName: api-service
servicePort: 80

deploymentpod ready后,访问你配置的域名,就可以看到https加密后的tomcat主页了。

Update

  • 2019-09-08: 添加了let’s encrypt部分缺失的安装步骤。

Zsh命令行历史同步插件推荐

公司和家里用的是两台电脑,经常遇到一些用过却记不住的命令,恰巧这些命令在另外台电脑执行过,就想找个能在不同机器上同步命令行历史的插件。

尝试过将用户目录文件夹下的.zsh_history文件软链接到dropbox,发现还是有点问题,比如多台机器同步是会发生文件冲突,一些情况下zsh会重新创建新的.zsh_history文件,导致数据丢失。期间又尝试了下history-sync这个插件,发现有会将历史文件清空的BUG。

无意中发现了zsh-histdb,这个插件正好满足了我的要求:在多台电脑之间同步历史记录,自动合并冲突文件。配合zsh-autosuggestions简直完美。

Docker环境访问fontello.com无限重定向问题

前阵子要把公司几个年代比较久远的前端项目迁移到新的jenkins环境,由于这些项目打包依赖库的版本比较老,要配置好环境会比较花时间,以后再迁移环境的话还要在搭建一遍,于是就想着用docker来打包项目。

本地搭建好docker环境后,测试打包的时候发现项目从fontello.com下载字体是老是会报一个超出最大重定向次数的异常,ssh到docker环境发现 wget -vdS fontello.com 返回的Response状态码一直是302,而本机环境和服务器都没这问题,docker环境wget其他网址也没有问题,一开始以为是fontello网站配置的问题,今天突然冒出一个想法会不会是我docker环境配置了代理的原因,于是将 fontello.com 域名加到了代理白名单里,发现就没有重定向异常了,字体文件能正常下载了。

这个问题的根本原因很有可能是docker代理功能和 fontello.com 两边的问题,在这里记一下,以防下次再遇到类似问题。

计算机网络(第七版)习题4-13计算过程

网上计算机网络的习题答案不是复制黏贴就是排版糟糕,简直不是给人看的。今天做这题目花了不少时间,就顺便把计算过程记录下来,希望帮助到对这题有疑惑的人。

包含IP部分,首部长度正好为为5个4字节。

  1. 现将数据转换成二进制格式
1
2
3
4
5
0100 0101 0000 0000 0000 0000 0001 1100
0000 0000 0000 0001 0000 0000 0000 0000
0000 0100 0001 0001
0000 1010 0000 1100 0000 1110 0000 0101
0000 1100 0000 0110 0000 0111 0000 1001
  1. 计算 0100 0101 0000 0000 + 0000 0000 0001 1100
1
2
3
4
5
0100 0101 0000 0000
+
0000 0000 0001 1100
=
0100 0101 0001 1100
  1. 计算 0100 0101 0001 1100 + 0000 0000 0000 0001
1
2
3
4
5
0100 0101 0001 1100
+
0000 0000 0000 0001
=
0100 0101 0001 1101
  1. 计算 0100 0101 0001 1101 + 0000 0000 0000 0000
1
2
3
4
5
0100 0101 0001 1101
+
0000 0000 0000 0000
=
0100 0101 0001 1101
  1. 计算0100 0101 0001 1101 + 0000 0100 0001 0001
1
2
3
4
5
0100 0101 0001 1101
+
0000 0100 0001 0001
=
0100 1001 0010 1110
  1. 计算 0100 1001 0010 1110 + 0000 1010 0000 1100
1
2
3
4
5
0100 1001 0010 1110
+
0000 1010 0000 1100
=
0101 0011 0011 1010
  1. 计算 0101 0011 0011 1010 + 0000 1110 0000 0101
1
2
3
4
5
0101 0011 0011 1010
+
0000 1110 0000 0101
=
0110 0001 0011 1111
  1. 计算 0110 0001 0011 1111 + 0000 1100 0000 0110
1
2
3
4
5
0110 0001 0011 1111
+
0000 1100 0000 0110
=
0110 1101 0100 0101
  1. 计算 0110 1101 0100 0101 + 0000 0111 0000 1001
1
2
3
4
5
0110 1101 0100 0101
+
0000 0111 0000 1001
=
0111 0100 0100 1110
  1. 将上面的结果取反得出:
1
1000 1011 1011 0001

AWS Cloudformation 踩坑记录

最近用cloudformation时遇到了一些坑,在此记录一下。

Conditions变量无法直接当布尔类型使用

Conditions可以动态控制一些资源的创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Conditions: 
CreateProdResources: !Equals [ !Ref EnvType, prod ]
Resources:
EC2Instance:
Type: "AWS::EC2::Instance"
Properties:
ImageId: !FindInMap [RegionMap, !Ref "AWS::Region", AMI]
MountPoint:
Type: "AWS::EC2::VolumeAttachment"
Condition: CreateProdResources
Properties:
InstanceId:
!Ref EC2Instance
VolumeId:
!Ref NewVolume
Device: /dev/sdh
NewVolume:
Type: "AWS::EC2::Volume"
Condition: CreateProdResources
Properties:
Size: 100
AvailabilityZone:
!GetAtt EC2Instance.AvailabilityZone

但是如果在其他地方像这样使用会报Unresolved resource dependencies CreateResources错误

1
2
3
4
5
6
7
8
/xxx/xx/xx.conf:
source:
!Sub https://${bucket}.s3.amazonaws.com/${prefix}/xx.conf
context:
create_resources: !Ref CreateResources
mode: "000644"
owner: root
group: root

解决办法就时将create_resources: !Ref CreateResources 改成 create_resources: !If [CreateResources, "true output ", "false output"]

aws-resource-init-files 传递布尔变量

AWS::CloudFormation::Init中 files属性可以用来创建文件,filescontext属性可以传递变量,来动态生成文件内容.
AWS文档里说文件是通过类似mustache的方式来处理的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
A typical Mustache template:

Hello {{name}}
You have just won {{value}} dollars!
{{#in_ca}}
Well, {{taxed_value}} dollars, after taxes.
{{/in_ca}}
Given the following hash:

{
"name": "Chris",
"value": 10000,
"taxed_value": 10000 - (10000 * 0.4),
"in_ca": true
}
Will produce the following:

Hello Chris
You have just won 10000 dollars!
Well, 6000.0 dollars, after taxes.

这里有个坑就是布尔类型在cloudformation里时当做字符类型来传递的,AWS官方认为这是一个feature而不是bug: What you have observed is a known behaviour with the CloudFormation servie
例如:

xxx.conf

1
2
3
4
5
6
7
{{#create_resources}}
this is true
{{/create_resources}}

{{^create_resources}}
this is false
{{/create_resources}}

yaml配置文件

1
2
3
4
5
6
7
8
9
/xxx.conf:
authentication: S3BucketAccess
source:
!Sub https://${bucket}.s3.amazonaws.com/${prefix}/xxx.conf
context:
create_resources: !If [CreateResources, true, false]
mode: "000644"
owner: root
group: root

这里无论 CreateResourcestrue 还是 false,文件内容始终会是 this is true,因为配置文件里的 truefalse 都被转成了字符类型。

这里的解决办法比较hack,就是将!If [CreateResources, true, false]改成!If [CreateResources, [1], []]:

1
2
3
4
5
6
7
8
9
/xxx.conf:
authentication: S3BucketAccess
source:
!Sub https://${bucket}.s3.amazonaws.com/${prefix}/xxx.conf
context:
create_resources: !If [CreateResources, [1], []]
mode: "000644"
owner: root
group: root

这里主要参考了 mustache 文档里 Inverted Sections : https://mustache.github.io/mustache.5.html

aws-resource-init 资源更新问题

aws-resource-init-sources可以用来下载压缩文件并解压到EC2指定目录,但这里有个问题就是如果目标文件夹已存在,cloudformation会将两文件夹内容合并而不是替换文件夹!比如AWS部署的项目使用的依赖LibraryA 1.0,将library升级到Library A 1.1并通过cloudformation source下载并解压新本版的项目到目标目录后,目标目录的项目文件夹里会同时存在Library A1.01.1版本。

解决办法也很简单,在用sources下载前将目标文件夹移除。

AWS平台的服务总有一些奇怪的限制或bug,调试时总会花不少时间,在此记录一下,希望能帮助到遇到相同问题的人,节约大家时间。

Update

  • 2019-07-07 添加aws-resource-init资源更新问题