AWS CodePipelineが対応していないgitリポからのCI/CD構築

# 本記事の対象読者
* AWS CodePipelineに対応していないgitリポから、CodePipelineに乗せたい人
* AWS Lambdaでgitコマンドを使いたい人

# CI/CDしたい
とあるAWSプロジェクトで、ソースコード管理にBacklog gitを利用していました。
どうせならCI/CDを組んでしまおうと考えたのですが、AWS PipelineがBacklog gitに対応しておらず。。。

そこで、Backlogのwebhookを使ってS3にまでソースコードを連携する以下のようなアーキテクチャ構成としました。
アプリ自体はnuxt.jsで作成しており、buildしてS3にアップロード、静的WEBとして公開することがゴールです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/213288de-10dd-758f-8772-0cf8f7efd554.png)

# 実装環境
AWS Cloud9

* Serverless Frameworkを利用したので、node.js環境が必要
* Docker環境

# API Gateway, Lambdaの構築
Serverless Framworkを利用して作成しました。
Cloud9使うならLambdaデプロイできる機能あるじゃん！！
と言われそうですが、ymlファイル等で管理できる&ソースのgit管理が容易、という点で自分はServerless Frameworkいいな！って思いました。

severless.ymlの作成 → Lambdaで使うライブラリの用意 → Lambda関数コード
の順で説明していきます。

## serverless.ymlの作成

以下がAPI GatewayとLambdaをデプロイするためのserverless.ymlです。
下記サイトを参考にプロジェクトを作成しました。
<a href="https://qiita.com/horike37/items/b295a91908fcfd4033a2">[2] Serverless Frameworkの使い方まとめ</a>

```serverless.yml
service: backlog-to-s3

# frameworkVersion: "=1.67.0"

plugins:
  - serverless-kms-secrets

provider:
  name: aws
  runtime: python3.8
  stage: dev
  region: ap-northeast-1

  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:PutObject"
      Resource: "arn:aws:s3:::{your_bucket_name}/*"
    - Effect: "Allow"
      Action:
        - KMS:Decrypt
      Resource: ${self:custom.kmsSecrets.keyArn}

custom:
  kmsSecrets:
    file: ${file(kms-secrets.${opt:stage, self:provider.stage}.${opt:region, self:provider.region}.yml)}
    keyArn: 'arn:aws:kms:ap-northeast-1:***************:key/{your_key_id}'

functions:
  git_clone:
    handler: handler.git_clone
    memorySize: 128
    timeout: 300
    events:
      - http:
          path: git/push
          method: post
          async: true
          integration: lambda
          request:
            parameters:
              querystrings:
                payload: true
          cors:
            origin: "*"
            headers:
              - Content-Type
              - X-Amz-Date
              - Authorization
              - X-Api-Key
              - X-Amz-Security-Token
              - X-Amz-User-Agent
    environment:
        USER_NAME: git_user_name
        BUCKET_NAME: your_bucket_name
        S3_KEY_SUFFIX: your_s3_key/
        PASS: ${self:custom.kmsSecrets.file.secrets.GIT_PASSWORD}


```

* plugins
serverless-kms-secretsを利用して、後述するgitのパスワードを暗号化しました。
AWS KMSを利用して暗号化・復号化を行っています。

* provider
ServerlessFrameworkで使うサービス等を指定します。
今回はAWS Lambdaに持たせるサービスロールも含めて指定します。
ここでは、git cloneしたソースコードをS3に配置する権限、KMSを使ってパスワードを復号する権限を付与しています。

* custom
KMSでパスワードを暗号化すると、暗号化後の値が記載されたymlファイルが作成されます。
ここでは、そのymlファイルを指定してserverless.ymlの中で利用できるように設定しています。
暗号化、復号化は下記サイト参考。
<a href="https://dream-yt.github.io/post/serverless-kms-secrets-101/">[3] Serverless FrameworkでAWS KMS</a>

* functions
Lambda関数の設定をしていきます。
ServerlessFrameworkで扱う際の関数名を指定します。（実際に作成されるLambda関数名はやや異なります）
eventsの項ではLambdaのキックイベントを指定でき、ここではAPI Gatewayの設定を行っています。
environmentの項では、Lambdaに持たせる環境変数を設定しています。
ポイントとしては、
    * events: async  
    API Gatewayは最大で29秒までしかリスポンスを待ってくれないので、念のため非同期でLambdaをキックするようにしています。
    * events: cors  
    Backlog gitのwebhookからAPIが叩かれるため、CORSの設定をしておきます。  
    * environment: PASS  
    GIT_PASSWORDというキーで暗号化したパスワードを呼び出しています。

## Lambdaで使うライブラリの用意

次に、ライブラリを準備します。
gitコマンドを使うために必要なライブラリをそろえるため、下記サイトを参考にDockerイメージからコピーしてきます。
<a href="https://qiita.com/qualitia_cdev/items/53a2b97e09e48d004b33">[4] Lambdaでgitコマンドを使う方法</a>

詳しくは上記を参考にしていただきたいですが、自分の場合/usr/share/git-coreが不足していてLambda実行時にエラーが発生したため、build.shに追記をしています。

```build.sh
#!/bin/sh

OUTPUT_PATH=${OUTPUT_PATH:-output}

yum install -y git
cp -a /usr/bin/git ${OUTPUT_PATH}
cp -a /usr/libexec/git-core/git-remote-https ${OUTPUT_PATH}
cp -a /usr/libexec/git-core/git-remote-http ${OUTPUT_PATH}
cp -a /usr/share/git-core ${OUTPUT_PATH}

```

少し怖かったので、一旦別のディレクトリにgitライブラリをコピーしてきたのち、別途ServerlessFrameworkのプロジェクトディレクトリに配置しました。

## Lambda関数コード
Lambda関数のコードは以下です。
serverless.ymlのhandlerで指定したメソッドから実行がスタートします。（git_clone）

```handler.py
import json
import os
import subprocess
import urllib
import boto3
import base64
import shutil
import datetime
now = datetime.datetime.now()
now = now.strftime("%Y%m%d%H%M%S")


kms = boto3.client('kms')
password = kms.decrypt(
        CiphertextBlob=base64.b64decode(os.getenv('PASS'))
    )['Plaintext'].decode('utf-8')

# Lambda関数は前回実行時のインスタンスを再度使用する場合があり、ディレクトリ名などがかぶることがあるので対策する
tmpdir = "/tmp/" + now
os.makedirs(tmpdir)
target = tmpdir + "/{your_git_repo_name}"

# ユーザー名がメアドだったりすると「@」でバグがでるのでURIエンコードする
username = os.environ['USER_NAME'].replace("@","%40")
source_bucket = os.environ['BUCKET_NAME']

def zip_files():
    print('ソースコードを zip で圧縮します.')
    try:
        shutil.make_archive(target, 'zip', target)
    except Exception as e:
        print(e)

def upload_to_s3(env):
    print('圧縮したソースコードを s3 にアップロードします.')

    zip_file_name = target.split('/')[-1]
    s3_key = env + "-" + os.getenv("S3_KEY_SUFFIX")

    s3 = boto3.client('s3')
    try:
        s3.upload_file(target + '.zip', source_bucket, s3_key + zip_file_name + '.zip')
    except Exception as e:
        print(e)

def git_clone(event, context):
    # Backlogのwebhookからブランチ名を取り出す
    print(event)
    body = event["body"]
    branch = body["content"]["ref"].split("/")[-1]

    # master, developブランチへの変更のみdeployしたかった
    if branch in ["master","develop"]:
        print("deploy start")
        root = os.path.abspath(os.path.join(__file__, ".."))
        print("root : {}".format(root))
        os.environ["HOME"] = "/tmp"
        os.environ["GIT_TEMPLATE_DIR"] = os.path.join(
            root, "git-core/templates"
        )

        repo_url = '{your_clone_url_https}'
        print(repo_url + ' からソースコードを取得します.')

        parsed_url = urllib.parse.urlparse(repo_url)
        src = parsed_url.scheme + '://' + username + ':' + password + '@' + parsed_url.netloc + parsed_url.path

        os.chdir(tmpdir)
        print("git clone start")
        subprocess.call([os.path.join(root, "git"),f"--exec-path={root}","clone","--depth","1","--branch", branch, src])
        print("git clone end")
        os.chdir(target)
        print("branch check")
        subprocess.call([os.path.join(root, "git"),f"--exec-path={root}","branch","-a"])

        zip_files()
        env = "prd" if branch == "master" else "dev"
        upload_to_s3(env)

        # 後処理
        os.chdir(root)
        shutil.rmtree(tmpdir)
    else:
        print("not deploy target brach")

    response = {
        "statusCode": 200,
        'headers':{
            "Access-Control-Allow-Origin":"*"
        }
    }

    return response

```
developとmasterブランチでそれぞれ別々のS3パスにソースコードをアップロードして、発火させるCodePipelineが別になるようにしています。
CodePipelineを2本作成し、それぞれを検証環境、本番環境へのデプロイに対応させています。

また、前回実行時と同じインスタンスが使用された時の挙動に注意です。
tmpディレクトリ内のファイル名は気を付けていましたが、カレントディレクトリも同じ状態で引き継がれるようです。。。

上記ファイルが準備できたら、Lambda関数をデプロイします。

```shell
serverless deploy -v
```
ここまでで一苦労、、、
でもここからは割と楽だった

# CodePipeline, CodeBuildの構築
まずは、CodeBuildで利用するbuildspec.ymlを作成します。
このymlファイルでは、build時のコマンドなどを設定します。
buildに必要なパッケージ等がある場合などにも、ここに記載してインストールします。
すこし雑ですが、今回は必要最低限の以下の内容としました。

```buildspec.yml
version: 0.2

phases:
  build:
    commands:
      - cd ./{your_nuxt_project_name}
      - npm install
      - npm run build
    finally:
      - |
        if [ ${ENVIRONMENT} -eq 'prd' ]; then
          aws s3 sync --exact-timestamps --delete ./dist/ s3://{your_bucket_for_prd}/
        else
          aws s3 sync --exact-timestamps --delete ./dist/ s3://{your_bucket_for_dev}/
        fi
      - |
```

上記のbuildspec.ymlをリポジトリのルートディレクトリに配置しておきます。

次に、CodePipelineをコンソールから作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/da6b09ea-7513-0a95-1512-9ef52f34229b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/3491e453-c494-d1a2-86f8-1c6cee785d1a.png)
↑ ソースコードを配置するS3パスを指定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/805fa2d8-36b9-31d7-c215-9c43441f92c8.png)
↑ ここで、「プロジェクトを作成する」から新しいCodeBuildプロジェクトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/14512ef3-18b5-e762-7033-beda3e8df176.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/93bc6ad9-355c-9dbe-938c-dee917ba76d4.png)
↑ サービスロールにはS3へのPUT権限が必要です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/248a714d-240d-a494-fcaf-e90b3b1d63bc.png)
CodePipelineに戻ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/22950c29-a7a5-ec2d-a92d-1944a5a00815.png)
↑ 環境変数を設定することで、buildspec.yml内で利用できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/545043/304d53c8-21fe-58b1-6aeb-cfe9d6b537cc.png)
↑ 今回、build, S3へのデプロイをCodeBuildで行うため、デプロイステージはスキップします。

以上で構築は完了です！
Backlog gitのmaster, developに変更が加わると本番・検証環境用のS3バケットに自動デプロイされます！

# まとめ
CodePipelineに対応していないgitリポジトリからもAWSへCI/CDできました！
Circle CI等、外部ツールを使う方法も今後試してみようと思います！

# 参考
<a href="https://dev.classmethod.jp/articles/aws-cdk-codepipeline/#toc-7-backloggit">[1] Backlog gitからAWS CodePipeline</a>
<a href="https://qiita.com/horike37/items/b295a91908fcfd4033a2">[2] Serverless Frameworkの使い方まとめ</a>
<a href="https://dream-yt.github.io/post/serverless-kms-secrets-101/">[3] Serverless FrameworkでAWS KMS</a>
<a href="https://qiita.com/qualitia_cdev/items/53a2b97e09e48d004b33">[4] Lambdaでgitコマンドを使う方法</a>