Oracle Function是Oracle Cloud Infrastructure对Fn的实现。
学习Oracle Function,如果要实践的话,Oracle官方提供的Oracle Functions QuickStart是最佳途径。这个QuickStart有网页版,文字版和PDF版。
Oracle Blogs也有一篇对应的文章:Oracle Functions: Serverless On Oracle Cloud - Developer’s Guide To Getting Started (Quickly!)。
简单来说,这个实验需要一个Oracle云账户和一台Fn客户机。Oracle云账户中我们需要用到Docker Registry和Fn服务,可以认为是服务端;Fn客户机上需要安装Fn Client,Docker Client,以及需要配置到服务端的认证。为了方便,我们使用Oracle云中的计算实例来充当Fn客户端。
开始是一些准备工作,如创建用户,组,Compartment,VCN等。此略。
在OCI中新建一实例,我用的Oracle Cloud Developer Image. 此Image中Docker已经安装,Fn CLI后续安装。
在root compartment,创建Policy FaaSPolicy
。定义如下,注意其中指定的compartment不能是child compartment:
Allow service FaaS to read repos in tenancy
Allow service FaaS to use virtual-network-family in compartment cp_mrd1_internal
创建Auth token,并马上拷贝留存,后续将用于docker login。
创建一个Oracle Docker Registry,本例为fnrepo(名字只能小写), 类型为public。
登录到OCI Docker Registry,其中fra为region code,口令为刚创建的auth token,注意用户名之前必须加上tenancy:
$ docker login fra.ocir.io
Username (tenancy/oracleidentitycloudservice/username):
Password: <your_auth_token>
WARNING! Your password will be stored unencrypted in /home/opc/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
安装Fn CLI:
$ curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh
fn version 0.5.92
______
/ ____/___
/ /_ / __ \
/ __/ / / / /
/_/ /_/ /_/`
$ fn version
Client version is latest version: 0.5.92
Server version: ?
接下来需要配置Fn CLI到服务端的认证。
生成Fn私钥:
$ mkdir ~/.oci
# 必须输入pass phrase,假设为1234
$ openssl genrsa -out ~/.oci/fn_key.pem -aes128 2048
$ chmod go-rwx ~/.oci/fn_key.pem
# 生成公钥,使用之前输入相同的pass phrase
$ openssl rsa -pubout -in ~/.oci/fn_key.pem -out ~/.oci/fn_key_public.pem
将公钥输出并拷贝到粘贴板,然后在OCI Console中添加API Key。
$ cat ~/.oci/fn_key_public.pem | pbcopy
编辑配置文件:
$ cat ~/.oci/config
[FaaS]
user=ocid1.user.oc1..aaaa...qfka
fingerprint=...:ce:ae
key_file=/home/opc/.oci/fn_key.pem
tenancy=ocid1.tenancy.oc1..aaaa...6p4q
region=eu-frankfurt-1
pass_phrase=1234
# 创建context,provider表示认证方式
$ fn create context FnContext --provider oracle
Successfully created context: FnContext
# 使用context
$ fn use context FnContext
Now using context: FnContext
# 指定认证的profile,FaaS是之前编辑的Profile名
$ fn update context oracle.profile FaaS
Current context updated oracle.profile with FaaS
# 设置compartment ID
$ fn update context oracle.compartment-id ocid1.compartment.oc1..aaaa...w7la
Current context updated oracle.compartment-id with ocid1.compartment.oc1..aaaa...sw7la
# 设置api-url endpoint
$ fn update context api-url https://functions.eu-frankfurt-1.oci.oraclecloud.com
Current context updated api-url with https://functions.eu-frankfurt-1.oci.oraclecloud.com
# 设置Docker Repository,此处为fnrepo,之前已创建
$ fn update context registry fra.ocir.io/ocichina001/fnrepo
Current context updated registry with fra.ocir.io/ocichina001/faasrepo
在OCI中,Developer Service>Applications中新建一应用,指定应用名为helloworld-app,并指定3个子网即可。后续应用会与function关联。
创建function, 本例使用Oracle自带的示例函数:helloworld-func,详见这里:
$ fn init --runtime java helloworld-func
Creating function at: ./helloworld-func
Function boilerplate generated.
func.yaml created.
然后就是部署function了,此命令会在OCI Registry中创建docker image,会将function与之前建立的应用关联:
$ cd helloworld-func
$ fn deploy --app helloworld-app
$ fn deploy --app helloworld-app
Deploying helloworld-func to app: helloworld-app
Bumped to version 0.0.2
Building image fra.ocir.io/ocichina001/fnrepo/helloworld-func:0.0.2 ...............................................................
Parts: [fra.ocir.io ocichina001 fnrepo helloworld-func:0.0.2]
Pushing fra.ocir.io/ocichina001/fnrepo/helloworld-func:0.0.2 to docker registry...The push refers to repository [fra.ocir.io/ocichina001/fnrepo/helloworld-func]
0f55b4ee3029: Pushed
5100e8eda33b: Pushed
8893b1d4ff35: Pushed
2ed87b0417a9: Pushed
543913bc2941: Pushed
6f2a80288c03: Pushed
0a0f0213cedd: Pushed
4b7c55f086f0: Pushed
b67d19e65ef6: Pushed
0.0.2: digest: sha256:d057b00a7b6db48f74cbe8a2297cd13218ec2219c465552a4b3e544afe459c5c size: 2206
Updating function helloworld-func using image fra.ocir.io/ocichina001/fnrepo/helloworld-func:0.0.2...
Successfully created function: helloworld-func with fra.ocir.io/ocichina001/fnrepo/helloworld-func:0.0.2
下图显示,应用与function已关联,另外你也可以看到Fn的命令:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
fra.ocir.io/ocichina001/fnrepo/helloworld-func 0.0.2 66d35725ed42 About a minute ago 222MB
<none> <none> b4c6dc0310e7 About a minute ago 440MB
fnproject/fn-java-fdk jre11-1.0.104 928cfe6738e1 2 months ago 222MB
fnproject/fn-java-fdk-build jdk11-1.0.104 72dcd80ee5ef 2 months ago 438MB
下图显示docker image已上传到Registry:
手工调用function,第一次时间很长,因为要创建和启动容器(称为warm start时间),后面就快了。所以说的Fn适合间歇性(抽风)的任务:
$ time fn invoke helloworld-app helloworld-func
Hello, world!
real 0m27.694s
user 0m0.092s
sys 0m0.014s
$ time fn invoke helloworld-app helloworld-func
Hello, world!
real 0m0.847s
user 0m0.093s
sys 0m0.010s
$ time fn invoke helloworld-app helloworld-func
Hello, world!
real 0m0.468s
user 0m0.091s
sys 0m0.012s
不过这个容器是会过期的,第二天早上执行又慢了,然后就快了。这也可以理解,毕竟你不用时是不用付费的。
来看一下代码,到这里就会发现,配置Fn是简单的,而最难的还是写代码:
$ vi ./main/java/com/example/fn/HelloFunction.java
$ cat ./main/java/com/example/fn/HelloFunction.java
package com.example.fn;
public class HelloFunction {
public String handleRequest(String input) {
String name = (input == null || input.isEmpty()) ? "world" : input;
return "Hello, " + name + "!";
}
}
$ cat ./test/java/com/example/fn/HelloFunctionTest.java
package com.example.fn;
import com.fnproject.fn.testing.*;
import org.junit.*;
import static org.junit.Assert.*;
public class HelloFunctionTest {
@Rule
public final FnTestingRule testing = FnTestingRule.createDefault();
@Test
public void shouldReturnGreeting() {
testing.givenEvent().enqueue();
testing.thenRun(HelloFunction.class, "handleRequest");
FnResult result = testing.getOnlyResult();
assertEquals("Hello, world!", result.getBodyAsString());
}
}
本例参照了此文的最后部分。
之前的helloworld-func是系统自带的Java函数。这里我们自定义一个NodeJS函数。由于一个应用可关联多个函数,因此我们将这个函数部署到与之前Java函数相同的应用中。
$ fn init --runtime node faas-demo-func-1
Creating function at: ./faas-demo-func-1
Function boilerplate generated.
func.yaml created.
$ cd faas-demo-func-1/
$ ls
func.js func.yaml package.json
$ cat func.js
const fdk=require('@fnproject/fdk');
fdk.handle(function(input){
let name = 'World';
if (input.name) {
name = input.name;
}
return {'message': 'Hello ' + name}
})
$ fn deploy --app helloworld-app
Deploying faas-demo-func-1 to app: helloworld-app
Bumped to version 0.0.2
Building image fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1:0.0.2 ................
Parts: [fra.ocir.io ocichina001 fnrepo faas-demo-func-1:0.0.2]
Pushing fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1:0.0.2 to docker registry...The push refers to repository [fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1]
c7db0a1328e2: Pushed
7d9c19fb39de: Pushed
22030c3d6056: Pushed
a0d7b4199dce: Pushed
8aed3db29123: Pushed
9c85c117f8f6: Pushed
a464c54f93a9: Pushed
0.0.2: digest: sha256:4c5bdbc5038083fad9da67fd4009eca1d93fd0441222ba997d36b035a584e1b8 size: 1780
Updating function faas-demo-func-1 using image fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1:0.0.2...
Successfully created function: faas-demo-func-1 with fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1:0.0.2
$ time fn invoke helloworld-app faas-demo-func-1
{"message":"Hello World"}
real 0m6.296s
user 0m0.086s
sys 0m0.017s
$ time fn invoke helloworld-app faas-demo-func-1
{"message":"Hello World"}
real 0m0.333s
user 0m0.091s
sys 0m0.011s
再进一步,我们配置一个环境变量,然后让函数读取此环境变量:
$ fn update app helloworld-app --config defaultName=Person
app helloworld-app updated
# 修改func.js代码如下:
$ cat func.js
const fdk=require('@fnproject/fdk');
fdk.handle(function(input){
let name = process.env.defaultName || 'World';
if (input.name) {
name = input.name;
}
return {'message': 'Hello ' + name}
})
# 重新部署
$ fn deploy --app helloworld-app
Deploying faas-demo-func-1 to app: helloworld-app
Bumped to version 0.0.3
Building image fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1:0.0.3 ..
Parts: [fra.ocir.io ocichina001 fnrepo faas-demo-func-1:0.0.3]
Pushing fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1:0.0.3 to docker registry...The push refers to repository [fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1]
2a3cc90cce76: Pushed
e61504cfda16: Pushed
22030c3d6056: Layer already exists
a0d7b4199dce: Layer already exists
8aed3db29123: Layer already exists
9c85c117f8f6: Layer already exists
a464c54f93a9: Layer already exists
0.0.3: digest: sha256:3c5448530391b93aa2b6bc1d0e901351fc26bcce610d7ffa8ee240dcdac3ef44 size: 1780
Updating function faas-demo-func-1 using image fra.ocir.io/ocichina001/fnrepo/faas-demo-func-1:0.0.3...
# 测试
$ time fn invoke helloworld-app faas-demo-func-1
{"message":"Hello Person"}
real 0m3.671s
user 0m0.093s
sys 0m0.012s
$ time fn invoke helloworld-app faas-demo-func-1
{"message":"Hello Person"}
real 0m0.639s
user 0m0.095s
sys 0m0.007s
补充一些Fn的命令,详见这里:
$ fn list contexts
CURRENT NAME PROVIDER API URL REGISTRY
* FnContext oracle https://functions.eu-frankfurt-1.oci.oraclecloud.com fra.ocir.io/ocichina001/fnrepo
default default http://localhost:8080
$ cat ~/.fn/contexts/FnContext.yaml
api-url: https://functions.eu-frankfurt-1.oci.oraclecloud.com
oracle.compartment-id: ocid1.compartment.oc1..aaaa...sw7la
oracle.profile: FaaS
provider: oracle
registry: fra.ocir.io/ocichina001/fnrepo
$ fn ls app
NAME ID
helloworld-app ocid1.fnapp.oc1.eu-frankfurt-1.aaaaaa...itbkviy36nxa
$ fn list fn helloworld-app
NAME IMAGE ID
helloworld-func fra.ocir.io/ocichina001/fnrepo/helloworld-func:0.0.2 ocid1.fnfunc.oc1.eu-frankfurt-1.aaaaaaa...jy2rvya
补充一个Docker的命令,以便知道你登录的Docker Registry:
$ cat ~/.docker/config.json
{
"auths": {
"fra.ocir.io": {
"auth": "b2NpY...OVU="
}
},
"HttpHeaders": {
"User-Agent": "Docker-Client/18.09.8-ol (linux)"
}
}$