FunctionComputeでもCICDしたい！

この記事は[Alibaba Cloud Advent Calendar 2019](https://qiita.com/advent-calendar/2019/alibabacloud) 11日目の記事です。

## はじめに
FunctionComputeでもインフラをコードで管理したいし、テストが成功したらAPIを自動でデプロイしてほしいですよね。
今ならTerraformも対応しているようだけど、公式ドキュメントで紹介されている[Fun]()を使ってCICD環境を構築できないだろうか。
そういえば、CIツールとして先日公式リリースになったばかりのGithubActionsがちょっと気になっていたのだった。
という思惑の元、CICDを通してHelloWorldするまでの記録です。

## システムアーキテクチャ
AWSLambda等でもよく見る構成で、認証もついてない雑なAPIです。
![FC_CICD_Architecture-Page-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/74dd5efd-103c-ce97-441b-41c590a6e373.png)

## 今回触れないこと
サンプルのAPIはNode.jsで用意していますが、実装とテストの中身については本項では省略します。
FunctionComputeにデプロイするAPIの実装は、ただのHelloWorldでも発火トリガーの種類や、APIGatewayと組み合わせるのかで実装が変わってくるのですが、こちらの説明も省略します。

## Funについて
Funは、AlibabaCloudのコミュニティで開発されているサポートツールです。
FunctionComputeとAPIGatewayの環境をYAML形式のファイルで定義してローカルからコマンドを実行すると、AlibabaCloud上に定義通りに環境を生成してくれます。Terraformでやれることと一緒ですね。
ただ、Terraformと違って環境を壊したりはできないみたいなので、お試しで作った環境をWEBコンソールまで削除しに行かないといけないのが少し面倒です。
また、FunはFunctionComputeの環境を構築すると同時に、APIの実装もデプロイします。
実行ランタイムにNodejsを選択していた場合、`node_modules`もまとめて圧縮してアップロードしてくれます。

なお、[GitHubリポジトリのREADME](https://github.com/alibaba/funcraft)が中国語なのを嫌って[公式日本語ドキュメント](https://jp.alibabacloud.com/help/doc-detail/64204.htm?spm=a21mg.l28256.b99.119.47066f2569xqM3)通りに設定を進めるとうまく動きませんでした。よく見るとREADMEとは記載が微妙に異なっていて、Funの開発が積極的に動いているので、公式ドキュメント（中国語の公式ドキュメントが整ってから反映される日本語ドキュメントは特に）の更新が遅れているようです。

### 基本操作
README読めばわかりますが…
`npm install @alicloud/fun -g`でインストール
`fun config`でAlibabaCloudのリソースへのアクセス権限を設定してから、`fun deploy`でデプロイできます。
プロジェクトリポジトリのルートに`.env`ファイルを予め用意することで、`fun config`の実行は省略可能でした。

## 成果物
手っ取り早くモノが見たいという方のための[リポジトリ](https://github.com/showylee/fc-cicd-sample)

## 手順
1. APIの実装とテストを用意する
2. FunのAlibabaCloudリソースへのアクセス権限設定に必要な情報を用意
3. Funの設定ファイルを用意
4. Actionsの設定ファイルを用意
5. GithubにPushしてActionsの結果を確認
6. AlibabaCloudのWEBコンソールからデプロイ結果を確認
7. 外部からアクセスできるか確認

### 1. APIの実装とテストを用意する
本体

```hello.js
'use strict';

module.exports.handler = function(event, context, callback) {
  const response = {
    "isBase64Encoded": false,
    "statusCode": 200,
    "headers": { "Content-Type": "application/json" },
    "body": "hello,world!"
  }
  callback(null, response);
}
```
テスト

```test/hello_test.js
const expect = require('expect')
const main = require('../hello.js')

const hoge = (sys, res)=> {
  result = res.body
}

let result = ""

describe('HelloTest', function(){
  it ('promise', () => {
    main.handler({}, "", hoge)
    expect(result).toBe('hello,world!')
  })
})
```

### 2. FunのAlibabaCloudリソースへのアクセス権限設定に必要な情報を用意

- AlibabaCloudのAccountID

こちらはRAMアカウントではなく、ROOTアカウントの物が必要になります。
<img width="1419" alt="スクリーンショット 2019-12-12 13.53.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/850c3c58-456b-bc8c-e072-0a198c6a578a.png">


- RAMユーザーとAK

ResourceAccessManagementで、Webコンソールログインが出来ないFun用のユーザーとAKを作成しました。
権限はRAM、APIGateway、FunctionComputeのFullAccess権です。
<img width="1419" alt="スクリーンショット 2019-12-12 13.56.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/463e5e98-ccea-d6b5-9dcc-f737bda9b1e7.png">

RAMのアクセス権は要らないと思うんだけど、これをつけないとFunコマンドを実行した際にRAM関連のエラーメッセージがでました。なんのために必要なんだ…？？？

また、アクセスログを取るためにLogServiceを使うなど、別サービスの設定をFunに追加する場合は、適宜このRAMユーザーに利用するサービスの権限を付与する必要があります。

### 3. Funの設定ファイルを用意

```template.yml
ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
  CICDSampleFunctionCompute:
    Type: 'Aliyun::Serverless::Service'
    CICDSample:
      Type: 'Aliyun::Serverless::Function'
      Properties:
        Handler: hello.handler
        CodeUri: './'
        Description: 'return string "hello,world!"'
        Runtime: nodejs10
        Timeout: 60
  CICDSampleAPIGateway:
    Type: 'Aliyun::Serverless::Api'
    Properties:
      StageName: TEST,RELEASE
      DefinitionBody:
        '/hello':
          get:
            x-aliyun-apigateway-api-name: sampleCICD
            x-aliyun-apigateway-fc:
              arn: acs::fc::service/${CICDSampleFunctionCompute.Arn}/functions/${CICDSample.Arn}/
              timeout: 10000
            x-aliyun-apigateway-request-config:
              requestMode: 'Mapping'
              requestProtocol: 'http'
```
### 4. Actionsの設定ファイルを用意

```.github/workflows/nodejs.yml
name: Node CI

on: [push]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [8.x, 10.x, 12.x]

    steps:
    - uses: actions/checkout@v1
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - name: npm install, build, and test
      run: |
        npm ci
        npm run build --if-present
        npm test
    - name: install funcraft
      run: |
        npm install @alicloud/fun -g
    - name: fun config echo to .env
      run: |
        echo ACCOUNT_ID=${{ secrets.ALICLOUD_ACCOUNT_ID }} > .env
        echo REGION=ap-northeast-1 >> .env
        echo ACCESS_KEY_ID=LTAI4Ftiwv1igVS2Whng3bfA >> .env
        echo ACCESS_KEY_SECRET=${{ secrets.ALICLOUD_TOKEN }} >> .env
        echo TIMEOUT=60 >> .env
        echo RETRIES=3 >> .env
    - name: deploy to AlibabaCloud FunctionCompute
      run: |
        fun deploy
```
FunのConfig設定は愚直に必要な項目を`.env`にechoしているんですが、これはもう少しなんとかならないかなぁと思います。
ACCESSKEY_SECRETなどの秘匿情報は、GithubのリポジトリSettingから名前をつけて設定しておくことで、 `${{secret.NAME}}`の形式で環境変数的に引っ張ってこれます。
<img width="1044" alt="スクリーンショット 2019-12-12 13.42.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/0c052c73-4c5c-2a1d-9c79-79520f99903c.png">

### 5. GithubにPushしてActionsの結果を確認
<img width="1413" alt="スクリーンショット 2019-12-03 17.42.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/088a4a9d-be1b-ede9-a24d-c1bdd5994c57.png">

ここで問題なのは、FunでDeployに失敗した場合、ErrorではなくAlibabaCloudからのレスポンスを表示するだけなので、GithubActionsとしてはグリーンになってしまう点です。
GithubActionsの結果だけ見てデプロイ出来たか否かがわからないのは大きい問題だと思います。なんとか判別する方法はないだろうか…（宿題）

### 6. AlibabaCloudのWEBコンソールからデプロイ結果を確認
<img width="1409" alt="スクリーンショット 2019-12-04 17.43.28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/207474f2-69a8-5154-2ae2-066913478243.png">

設定ファイルで指定した通り、テストと本番で実行中になっています。

### 7. 外部からアクセスできるか確認
APIGatewayのAPIグループからInternetSubdomainを確認します。これはAlibabaCloudがデフォルトで提供してくれるサブドメインです。画像に赤字であるように、1日1000回のアクセス制限があるのでサービスで採用する場合は独自ドメインをバインドしたほうがよいでしょう。なお、この独自ドメインのバインドもFunから設定できます。
<img width="1400" alt="スクリーンショット 2019-12-04 17.45.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/4075a6e1-4033-8ba1-8dc0-a6fc3fd544fc.png">
`http://InternetSubdomain/hello`で、`hello,world!`がレスポンスされることが確認できました。
<img width="1140" alt="スクリーンショット 2019-12-04 17.53.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/d696a8aa-f83f-4cac-7a1b-a3082ab337ce.png">


## おわりに
無事FunctionComputeでCICDできました。
インフラをコードで管理するの楽しいですし、自動化のための一歩を踏み出した感じでいいですよね。

今回GithubActionsを初めて使ってみて、CircleCIより設定も楽で、なによりリポジトリから直接お試しにいけるっていうのが、ハードル低くていいなと思いました。
Funには今回は試していないけど、実運用するなら考えなければいけないログとかドメインとかの部分に関する設定項目もたくさんあります。中国語でハードル高いですが、設定の変数名を眺めるだけでもなにができそうかはわかるので、ぜひ一度目を通してみてください。
APIGatewayの設定はSwaggerファイル指定も出来るので、APIをOASで管理するプロジェクトも多いみたいだし、この点は選択時の強みになるんじゃないでしょうか。Terraformでも同じことできるかどうかは知らないけど。
<img width="910" alt="スクリーンショット 2019-12-12 14.10.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/333391/5d6e077f-0baf-45f7-f8e3-03aa138d790e.png">