本記事の対象読者
- 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として公開することがゴールです。
実装環境
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の使い方まとめ
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というキーで暗号化したパスワードを呼び出しています。
- events: async
Lambdaで使うライブラリの用意
次に、ライブラリを準備します。
gitコマンドを使うために必要なライブラリをそろえるため、下記サイトを参考にDockerイメージからコピーしてきます。
[4] Lambdaでgitコマンドを使う方法
詳しくは上記を参考にしていただきたいですが、自分の場合/usr/share/git-coreが不足していてLambda実行時にエラーが発生したため、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)
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に必要なパッケージ等がある場合などにも、ここに記載してインストールします。
すこし雑ですが、今回は必要最低限の以下の内容としました。
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をコンソールから作成します。
↑ ソースコードを配置するS3パスを指定します。
↑ ここで、「プロジェクトを作成する」から新しいCodeBuildプロジェクトを作成します。
↑ サービスロールにはS3へのPUT権限が必要です。
CodePipelineに戻ります。
↑ 環境変数を設定することで、buildspec.yml内で利用できます。
↑ 今回、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コマンドを使う方法