5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

本記事の対象読者

  • 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

実装環境

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です。
下記サイトを参考にプロジェクトを作成しました。
[2] Serverless Frameworkの使い方まとめ

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の中で利用できるように設定しています。
    暗号化、復号化は下記サイト参考。
    [3] Serverless FrameworkでAWS KMS

  • 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イメージからコピーしてきます。
[4] Lambdaでgitコマンドを使う方法

詳しくは上記を参考にしていただきたいですが、自分の場合/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関数をデプロイします。

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
image.png
↑ ソースコードを配置するS3パスを指定します。
image.png
↑ ここで、「プロジェクトを作成する」から新しいCodeBuildプロジェクトを作成します。
image.png
image.png
↑ サービスロールにはS3へのPUT権限が必要です。
image.png
CodePipelineに戻ります。
image.png
↑ 環境変数を設定することで、buildspec.yml内で利用できます。
image.png
↑ 今回、build, S3へのデプロイをCodeBuildで行うため、デプロイステージはスキップします。

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

まとめ

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

参考

[1] Backlog gitからAWS CodePipeline
[2] Serverless Frameworkの使い方まとめ
[3] Serverless FrameworkでAWS KMS
[4] Lambdaでgitコマンドを使う方法

5
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?