【Alibaba Cloud】OSS / Function Compute でもSSIっぽいことをやる

# TL;DR
[前回](https://qiita.com/imaichiyyy/items/e216f691c02756d628a2)、AWSでSSI（include virtual）が記述されたHTMLを、S3の特定バケットに格納しLambdaでインクルード内のソースを結合した上で別のバケットに格納するような仕組みを作りましたが、それのAlibaba Cloud版です。

# やりたかったこと
OSS（Object Storage Service）は静的サイトホスティングサービスのため、サーバーサイド側で動的に処理をするということが原則できません。  
もちろんその後キャッシュする先であるCDN側（Alibaba Cloud CDN）もできません。  

その場合、今であればローカル開発環境を構築しローカル上では別々のファイルにしておき、コンパイルする際に結合する方法がスタンダードだと思いますが、元々SSIが使われていた既存サイトをOSS+CDNへ移管する場合などを含め残念ながらすべての案件でそのフローが導入できるわけでもありません。

なので、元々SSIが使われていた場合、そのまま置換など行わずそのままの形で使用できるようにAlibaba Cloud側で調整を試みました。

# やったこと

（前提としてAlibaba Cloud版のIAMであるRAMやKMSの設定が終わった状態です）

基本的に前回の記事を元に、Alibaba Cloud用にカスタマイズしています。
[【AWS】S3 / LambdaでSSIっぽいことをやる](https://qiita.com/imaichiyyy/items/e216f691c02756d628a2)
Alibaba Cloudの設定についてはSBクラウドさんのこちらの記事が役に立つ思います。
[イベント駆動サービス FucntionComputeでオブジェクトストレージを操る](https://www.sbcloud.co.jp/entry/2018/07/11/function-compute_intro/)

## 構成
前回と構成は同じです。
なので使用するサービスはOSSとFunction ComputeのみでAlibaba Cloud CDNは必要であれば。
![SSI_alibaba.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/161498/85c523dc-175f-8b8e-040a-5658b7f2f4dd.png)

## 設定方法
### OSS
Alibaba Cloud版ではファイルアップするバケットは1つにしています。
その中でtemp用のディレクトリと公開用のディレクトリの2つに分けて処理を行います。
#### 公開/temp用バケット
名前は何でも良いです。
今回は `oss-ssi-include` としています。

#### 各設定
アクセス許可はそれぞれ適宜設定されているものとします。

#### 公開用ディレクトリとtemp用ディレクトリの作成
公開用ディレクトリ `dist` とtemp用ディレクトリ `src` を「ディレクトリの作成」ボタンからそれぞれバケット内に作成してください。

### Function Compute

Function ComputeではPUTイベントを検知して、 `src/` ディレクトリ内にアップされたHTMLファイル内にSSI（サーバーサイドインクルード）が記述されていれば、Function Computeでインクルード処理を行い、 `dist/` ディレクトリに格納するような関数を作成します。

#### 関数コード
```python
import json
import os
import logging
import oss2 
import re
import urllib.parse

logger = logging.getLogger()
logger.setLevel(logging.INFO)

OSS_ENDPOINT        = "oss-ap-northeast-1.aliyuncs.com"
DEST_BUCKET_NAME    = "oss-ssi-include"

def handler(event, context):
    logger.info('## ENVIRONMENT VARIABLES')
    logger.info(os.environ)
    logger.info('## EVENT')
    logger.info(event)
    
    creds = context.credentials
    auth = oss2.StsAuth(creds.accessKeyId, creds.accessKeySecret, creds.securityToken)
    evt = json.loads(event)
 
    # 下記書き換える
    input_bucket = oss2.Bucket(auth, OSS_ENDPOINT, DEST_BUCKET_NAME)

    logger.info('## INPUT BUKET')
    logger.info(input_bucket)
    
    input_key = urllib.parse.unquote_plus(evt['events'][0]['oss']['object']['key'])
    logger.info('## INPUT KEY')
    logger.info(input_key)

    try:
        # 入力ファイルの取得
        response = input_bucket.get_object(input_key)
        logger.info(response)

        # ファイル出力
        output_key    = re.sub('^src/','dist/',input_key)
        logger.info('## OUTPUT KEY')
        logger.info(output_key)

        if not input_key.endswith('.html'):
            logger.info(response)
            input_bucket.put_object(output_key, response)

        else:
            input_html = response.read().decode('utf-8')
            logger.info('## input_html')
            logger.info(input_html)
            output_html = input_html
            # SSI記述を取得
            include_path_base = re.findall(r'<!--#include virtual="/(.*?)" -->.*?\n', input_html, flags=re.DOTALL)
            logger.info('## PATH BASE')
            logger.info(include_path_base)
            if len(include_path_base) > 0:
                for path in include_path_base:
                    include_path = path
                    logger.info('## PATH')
                    logger.info(include_path)
            
                    # SSIファイルの取得
                    try:
                        include = input_bucket.get_object('src/' + include_path)
                        include_html = include.read().decode('utf-8')
                        # SSIを実行
                        output_html = output_html.replace('<!--#include virtual="/' + include_path + '" -->', include_html)
                    except ClientError:
                        pass
            

            input_bucket.put_object(output_key, output_html)

    except Exception as e:
        logger.info(e)
        raise e
```

#### その他の設定
##### トリガー設定
トリガータイプ：OSS トリガー
トリガー名：put
イベントソース：
`acs:oss:ap-northeast-1:xxxxxxxxxxxxxxxxx:ossname`
イベント： `oss:ObjectCreated:PutObject` , `oss:ObjectCreated:PostObject`
トリガールール：接頭辞 `src/`
ロール操作：既存のロールを選択
既存のロール：
ロールはRAMで設定した以下のポリシーでOKです。

```
{
    "Version": "1",
    "Statement": [
        {
            "Action": [
                "fc:InvokeFunction"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```



# おわりに
[前回](https://qiita.com/imaichiyyy/items/e216f691c02756d628a2)、AWSでSSI（include virtual）が記述されたHTMLを、S3の特定バケットに格納しLambdaでインクルード内のソースを結合した上で別のバケットに格納するような仕組みを作りましたが、諸事情でAlibaba Cloudでも作る必要があったため用意してみました。

AWS版と違ってtemp用ディレクトリにファイルアップ→CloudComputeが処理→公開用ディレクトリにファイルが格納されるため、使い良い形にするのであれば公開用ディレクトリに格納されたファイルはCDNへ配信した方が良いと思います。

あと、日本でAlibaba Cloudの情報を得ようとする時はSBクラウドさんの[エンジニアブログ](https://www.sbcloud.co.jp/archive/category/techblog)がおすすめです。
今回のものも大分参考にさせていただきました。

ただこれって重要あるのかな。。
現場からは以上です。