2018年12月19日水曜日
circleciでビルドしたあとにnetlifyへデプロイしてみた
こんにちは!
シタテルでエンジニアをしている建山です。
主に工場向けのマイオペというシステムの開発を行っています。
マイオペでは、インフラとして本番、ステージング、開発、feature環境を用意して開発していますが、新機能をテストする環境としてfeature環境を使用しています。
そして、feature環境にnetlifyというサービスを使用しています。
netlifyとは
Netlifyは静的なサイトをすばやく提供できるWebサービスです。フロントエンドのビルド、デプロイ、ホスティングのすべてを行ってくれます。
今回の概要
デプロイの流れとしては、github -> circleci ➔ netlify
となっています。netlifyにはビルドする機能が備わっているので、github -> netlify
が可能なのですが、マイオペでは、ビルドの際にjavaを使用していて、netlifyのビルドで対応されていないので、circleciでビルドを行ったあと、netlifyにデプロイすることにしました。
今回の前提
- ビルドはcircleciでおこなう
- ビルドが終わったものをnetlifyにデプロイする
- 指定のサイトURLにデプロイする
「それ、netlifyじゃなくてAWS S3でよくない?」っていうツッコミはあるとおもいますが、まずは使ってみたということでご容赦ください。
方法
事前にnetlifyにサイトを作成してください。(画面やcliを使って作成できます)
netlify cliをインストールします。今回は、プロジェクトにnpm install --save netlify-cli
でいれました。circleciのconfig.ymlのstepsにnpm install -g netlify-cli
でインストールしてもいいと思います。
次に、circleciのconfig.ymlでデプロイの設定をします。
こんな感じで、インストールされたnetlify cliを使って、デプロイします。
<<他省略>>
# run deploy
- run:
name: deploy
command: |
if [[ "$CIRCLE_BRANCH" =~ ^.*-netlify.* ]]; then
node_modules/netlify-cli/bin/cli.js deploy --site-id $NETLIFY_SITE_ID -p ${SC_DIR}/ --access-token $NETLIFY_ACCESS_TOKEN
fi
${SC_DIR}
にはデプロイしたいディレクトリをいれてください。$NETLIFY_SITE_ID
と $NETLIFY_ACCESS_TOKEN
はcircleciの環境変数にセットしてお使い頂いたほうがいいかと思います。
上の例だと、ブランチ名に"-netlify"が含まれているとnetlifyにデプロイされるようにしています。
実際には、利用状況にあわせてカスタマイズしてください。
たとえば、"abcd-netlify"というブランチでgithubにpushすると、
デプロイされます!
今回は同じURLにデプロイしていますが、デプロイのたびに違うサイトURLを発行してデプロイもできるようです。
netlifyの中で、ビルドもいろいろできるようになっているので、今回のように限定的な使い方ではなく、もっと活用できると思います。
とりあえず使ってみましたということで、またいい感じのCI/CDを実現できたら紹介します!
詳しくは公式ドキュメントをご覧ください。https://www.netlify.com/docs/
2018年12月7日金曜日
CircleCIでAWS ECSに自動デプロイする(おまけで環境面の切り替えも)
こんにちは、シタテルの茨木です。
今回はCircleCIでDockerをビルドし、そのままAWS ECR/ECSにデプロイする為の設定例を紹介します。
Nuxt.jsアプリ用に作ったものですが、言語やAPサーバが違っても共通の部分がほとんどです。
PRマージからの自動デプロイ、いいですよね。
https://circleci.com/docs/2.0/ecs-ecr/
https://github.com/CircleCI-Public/circleci-demo-aws-ecs-ecr
基本的にはCircleCI公式ガイドの上記リンクを参考にしていますが、元ネタはterraform前提だったりでなかなか大仰です。(下記に説明漏れがあったら原典を見てください…)
今回紹介するのはリンク先のスクリプト類を簡素化し、少し機能を追加(リポジトリ名からデプロイ先の環境面を切り替えできるように)したものです。
以下で省略しているけれど必要なモノ
- ECS/ECRの設定
これだけで別の記事が書けてしまうので割愛します
ecsTaskExecutionRoleの割り当てとか忘れがちですかね
- githubリポジトリ
言わずもがな。CircleCIとの接続設定はされているものとします
- Dockerfile
リポジトリのルートに。Dockerfileの書き方は解説しません
- requirements.txt
リポジトリのルートに。awscliを使えるようにします
https://github.com/CircleCI-Public/circleci-demo-aws-ecs-ecr/blob/master/requirements.txt
- CircleCIの環境変数設定
github上にaccess_keyを晒さなくていいように、入れておきます
https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-project
この手順で設定した環境変数はCircleCIのジョブから参照できます
.circleci/config.yml:build
version: 2
jobs:
~省略~
build:
docker:
- image: circleci/node:10-stretch
steps:
- checkout
- setup_remote_docker
- run:
name: Setup common environment variables
command: |
echo 'export ECR_REPOSITORY_NAME="YOUR-ECR-REPO-NAME"' >> $BASH_ENV
echo 'export FULL_IMAGE_NAME="${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR=REPOSITORY_NAME}:${CIRCLE_SHA1}"' >> $BASH_ENV
- run:
name: Build image
command: |
docker build -t $FULL_IMAGE_NAME .
- run:
name: Save image to an archive
command: |
mkdir docker-image
docker save -o docker-image/image.tar $FULL_IMAGE_NAME
- persist_to_workspace:
root: .
paths:
- docker-image
~省略~
さっそくですがビルドジョブです。
- YOUR-HOGEHOGE箇所はご自身のECS/ECRの設定に合わせて置き換えてください
- image: circleci/node:10-stretch
node:10を使っていますが、これはビルドする対象がNuxt.jsアプリだからですね。Dockerビルド時に必要なモノに合わせて修正してください
.circleci/config.yml:deploy
~省略~
deploy:
docker:
- image: circleci/python:3.6.1
environment:
AWS_DEFAULT_OUTPUT: json
steps:
- checkout
- setup_remote_docker
- attach_workspace:
at: workspace
- restore_cache:
key: v1-{{ checksum "requirements.txt" }}
- run:
name: Install awscli
command: |
python3 -m venv venv
. venv/bin/activate
pip install -r requirements.txt
- save_cache:
key: v1-{{ checksum "requirements.txt" }}
paths:
- "venv"
- run:
name: Load image
command: |
docker load --input workspace/docker-image/image.tar
- run:
name: Setup target environment variables
command: |
echo 'export TARGET=`echo $CIRCLE_BRANCH | sed -e s/develop.*/dev/ -e s/release.*/stg/ -e s/master/prd/`' >> $BASH_ENV
source $BASH_ENV
- run:
name: Setup common environment variables
command: |
echo 'export ECR_REPOSITORY_NAME="YOUR-ECR-REPO-NAME"' >> $BASH_ENV
echo 'export ECS_CLUSTER_NAME="YOUR-ECS-CULSTER-NAME-PREFIX${TARGET}"' >> $BASH_ENV
echo 'export ECS_SERVICE_NAME="YOUR-ECS-SERVICE-NAME-PREFIX${TARGET}"' >> $BASH_ENV
- run:
name: Push image
command: |
. venv/bin/activate
eval $(aws ecr get-login --region ap-northeast-1 --no-include-email)
docker push $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/$ECR_REPOSITORY_NAME:$CIRCLE_SHA1
- run:
name: Deploy
command: |
. venv/bin/activate
export AWS_DEFAULT_REGION="ap-northeast-1"
export ECS_TASK_FAMILY_NAME="YOUR-ECS-TASK-NAME-PREFIX${TARGET}"
export ECS_CONTAINER_DEFINITION_NAME="YOUR-ECS-CONTAINER-DEF-NAME-PREFIX${TARGET}"
export EXECUTION_ROLE_ARN="arn:aws:iam::$AWS_ACCOUNT_ID:role/ecsTaskExecutionRole"
bash ./deploy.sh
~省略~
デプロイジョブです。
- ブランチ名から環境名へ
echo 'export TARGET=`echo $CIRCLE_BRANCH | sed -e s/develop.*/dev/ -e s/release.*/stg/ -e s/master/prd/`' >> $BASH_ENV
ブランチ名に応じた値(developブランチなら"dev")を環境変数に入れています。これをsuffixとして使い、デプロイ先のクラスタ名等に反映しています
- YOUR-HOGEHOGE箇所はご自身のECS/ECRの設定に合わせて置き換えてください
余談ですが、ECSはクラスタ・サービス・タスク・タスク定義といろいろ設定しないといけないのですが、なかなか直感的にわかりにくくてイマイチな印象です
.circleci/config.yml:workflows
~省略~
workflows:
version: 2
build-deploy:
jobs:
- build:
filters:
branches:
only:
- develop
- /release\/.*/
- master
- deploy:
requires:
- build
filters:
branches:
only:
- develop
- /release\/.*/
- master
この辺はよしなに
deploy.sh
#!/usr/bin/env bash
set -eo pipefail
# more bash-friendly output for jq
JQ="jq --raw-output --exit-status"
deploy_cluster() {
make_task_def
register_definition
if [[ $(aws ecs update-service --cluster $ECS_CLUSTER_NAME --service $ECS_SERVICE_NAME --task-definition $revision | \
$JQ '.service.taskDefinition') != $revision ]]; then
echo "Error updating service."
return 1
fi
# wait for older revisions to disappear
# not really necessary, but nice for demos
for attempt in {1..30}; do
if stale=$(aws ecs describe-services --cluster $ECS_CLUSTER_NAME --services $ECS_SERVICE_NAME | \
$JQ ".services[0].deployments | .[] | select(.taskDefinition != \"$revision\") | .taskDefinition"); then
echo "Waiting for stale deployment(s):"
echo "$stale"
sleep 30
else
echo "Deployed!"
return 0
fi
done
echo "Service update took too long - please check the status of the deployment on the AWS ECS console"
return 1
}
make_task_def() {
task_template='[
{
"name": "%s",
"image": "%s.dkr.ecr.%s.amazonaws.com/%s:%s",
"essential": true,
"portMappings": [
{
"containerPort": 80
}
],
"environment": [
{
"name": "TARGET",
"value": "%s"
}
]
}
]'
task_def=$(printf "$task_template" $ECS_CONTAINER_DEFINITION_NAME $AWS_ACCOUNT_ID $AWS_DEFAULT_REGION $ECR_REPOSITORY_NAME $CIRCLE_SHA1 $TARGET)
}
register_definition() {
if revision=$(aws ecs register-task-definition --requires-compatibilities FARGATE --cpu 256 --memory 1024 --network-mode awsvpc --execution-role-arn $EXECUTION_ROLE_ARN --container-definitions "$task_def" --family $ECS_TASK_FAMILY_NAME | $JQ '.taskDefinition.taskDefinitionArn'); then
echo "New deployment: $revision"
else
echo "Failed to register task definition"
return 1
fi
}
deploy_cluster
デプロイジョブから呼び出すスクリプトです。
task_templateの内容を色々弄るとカスタマイズできます
設定可能な項目はこちら
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/create-task-definition.html
ここでは、environmentで環境変数TARGETを設定しています
ECSがdocker runする際に引数として付与され、docker内のアプリから環境変数として参照可能になります
実際には、nuxt.config.js内でTARGETを参照し、APIサーバの向け先を切り替えたり等しています
以上、ご参考になれば幸いです。
2018年11月14日水曜日
リリース作業を少し楽にした開発(Slack+Lambda+Golang)
こんにちは、シタテルの鶴巻です。
devops&インフラ担当です。
シタテルのプロダクトのリリース作業を少し楽にするために、
Slack+Lambda+Golangを用いて開発した話をします。
リリースの流れ
シタテルでは、GitフローやGitHubフローを採用しています。
また、本番環境へのデプロイは、以下の流れで実施しています。
- developブランチからreleaseブランチを作成
- ステージング環境でreleaseブランチをデプロイ&動作確認
- 本番環境にデプロイ
ちなみに、1.でreleaseブランチを作成すると、CircleCIによって2.のステージング環境へのデプロイは自動で行われます。
地味に面倒なリリース作業
ステージング環境へのデプロイは自動化されているとはいえ、小さな手作業が合間に発生し、地味に面倒でした。
具体的には以下のような作業です。
- developブランチからreleaseブランチの作成
- ローカルPCでdevelopブランチを最新の状態にpullして、releaseブランチ作成してGitHubにpush
- releaseブランチのプルリクエスト作成
- GitHubのUIをポチポチ
- リリースタグの作成
- 前回のリリース以降でマージされたプルリクエストを見て、リリース内容を記述
手作業はSlackからコマンドを叩くだけにしました
Slackから以下のコマンドを叩くだけで、地味に面倒くさかった作業を自動で行うようにしました。
/release <repository> <release branch>
実際には、以下の内容を実施するGolangプログラムをLambdaで実行しています。
- 対象のリポジトリをクローンし、developブランチからreleaseブランチを作成し、GitHubにpush
- releaseブランチのプルリクエストを作成
- 前回のリリース以降にマージされたプルリクエストのタイトルのリストを記述したリリースタグのドラフトを作成
構成
Slack -> API Gateway -> Lambda01(同期) -> Lambda02(非同期) -> GitHub
- Slackからコマンドを叩くと、Lambda01にリクエストを送信します。
- Lambda01は、Lambda02に対象レポジトリとブランチ名を付与してリクエストを送信します。
- Lambda02で作業を実行します。
Lambdaを2つ使用している理由
Slackはリクエスト送信後3秒でタイムアウトするためです。
実行したいプログラムは3秒以上かかるため、実際の作業は非同期で実施する必要がありました。
そのため、Lambda01はLambda02に処理を投げて、Slackに200を返し、実際の処理はLambda02で実施します。
Lambda01はLambda02を非同期実行で呼び出すので、リクエストをキューに入れた後は、Lambda01は関数を終了することができます。
Lambdaの呼び出しタイプ(同期・非同期)
Lambdaは、呼び出しタイプ(InvocationType)を指定することで、同期実行や非同期実行を選択することができます。
例:InvocationTypeで"Event"を指定することで、非同期実行でLambdaを呼び出せます。
input := &lambda.InvokeInput{
FunctionName: aws.String("xxxx"),
Payload: jsonBytes,
InvocationType: aws.String("Event"),
}
※同期実行は"RequestResponse"を指定します
今回使用したGitHub操作のためのGoライブラリ
- go-git https://github.com/src-d/go-git
- インメモリでgitクローンするために使用
- go-github https://github.com/google/go-github
- google製
- プルリク作成やリリースタグ作成に使用
その他ハマったポイント
- APIGatewayの統合リクエストのマッピングテンプレートを使いこなせず。デバッグのやり方もわからず、使うのを諦めました。
まとめ
- 少しの手間が自動化されるだけで、すごく楽になります。これからももっとやっていきたいです。
- GitHub操作のライブラリはとても便利でした。
- シタテルのサービスはRails + vue.jsが主ですが、自分の好きなGo言語を使えたので楽しかったです。