昔は1台のサーバーにサイトをいくつもいれてデータベースは勿論共用、どちらもカリカリにチューニングして使っていました。
バックアップはテープにとっていて、リカバリとかなるとタダの地獄です。だいたいバックアップはとっていてもリカバリの予行練習なんてしてないしなー。
サーバーセンターで回線借りると1Mbpsあたり五万円くらいしてた様な気がします。
時代が進みサーバーを複数台使って冗長化できるようになるとアプリケーションの更新は複数台のサーバーにSSHでログインして同じ事しないといけなくなりました。
tmuxつかって同時作業してみたり、chef使って頑張ってみたり。そういやAWSでchef使うサービスあったなぁ・・・・。何だっけ。
そんな年寄りが感じるのは最近インフラを触っていてSSHを使う事がホント減ってきたなってことです。
(まあ、昔はサーバーセンターで寒い中OSインストールして直にサーバーを設定していたりもしましたが・・・)
そんなわけで今回AWSのECSとCodePipelineを使ってSSHを全く使わずにnginxのサーバーを展開してコンテンツを更新してみたいと思います。
できる限り分かりやすくシンプルにするのでCI/CDの入門として読んで頂ければ。(あまり正確には解説できていないですが)
dockerやaws cliの説明はないのでそのあたりはググって下さい。
ゴール
- ECSでWebサーバーを立ち上げる。
- CodeCommitにpushしたらコンテンツが更新される。
CI/CDってなんだろう?
自動でアプリケーションを動くようにソースコードから構築、テストまでしてくれて、サーバーにそれをいい感じで配置してくれるやつです。
例えばサーバーが100台あったら全部のサーバーに作ったアプリを置くの嫌ですよね?
ビルドとテストだって手作業で10分かかったとして一日10回やっていたら100分もPCの前でサボれる・・・じゃなくて、待つことになるの嫌ですよね?
これをやってくれる仕組みは沢山あるのですが今回は以下のサービスを使って行います。
- codeCommit
- gitリポジトリ。以上。
- codeBuild
- ビルドする間だけサーバーを貸してくれます。その中でビルドしたりテストします。
- Macインスタンス使えるようになって欲しい!!!
- codeDeploy(今回はこれ使わないでデプロイしちゃいます)
- EC2インスタンスにこれ用のエージェントが常に動いていてビルド済みのアプリのカタマリ(Artifactsという)をどっかから取ってきたり、それを配置したり、他にも色々できます。
- codePipeline
- 上の3つを連携させたりできます。codeCommitにpushされた!よし、codeBuildに渡したる。codeBuildでアプリ出来上がったぞ!よしcodeDeployへ渡したる。みたいな。
ECSってなんだろう?
コンテナを管理する仕組みです。基本は以下の3つで構成されています。
- タスク
- これで一つのアプリケーションみたいな感じ
- コンテナの集合
- 例えばPHPを動かすならnginxとphp-fpmのコンテナを連携させて動かすように設定する。
- タスク定義として設定する
- タスク定義 ≒ docker-compose.yml ってとこかな?
- サービス
- タスクを決めた数動かし続けてくれます。
- 他にも例えばどのロードバランサ使うのかとか、タスクをデプロイするときは1台ずつとか全部まとめてとか色々。
- サービスで動かせるタスクの種類は一つ。
- クラスタ
- サービスとタスク、インスタンスなどのカタマリ
- 一番大きな集合
箱入りの饅頭とか大福を想像するとちょっと分かりやすいかも?
- タスク ➡️ あんこ
- サービス ➡️ 皮
- クラスタ ➡️ 箱
サービスなしで手動でタスクだけをクラスタ上で動かすことも出来ますが混乱するので気にしない方がいいです。(自分はこいつのせいでサービスとタスクの関係がなんなのか理解しにくかったです)
ECSハンズオン
まずはnginxのコンテナをECS上で動かして表示を確認してみましょう。
今回はECSで使うインスタンスはFargateではなくてEC2にしています。
0. 今回のハンズオンでつかうコードをローカルに持ってくる
リポジトリURL: https://github.com/taichi0529/ecs-practice
git clone git@github.com:taichi0529/ecs-practice.git
cd ecs-practice
1. ECRへリポジトリをつくる
ECRはdockerのイメージを管理してくれるAWS上のリポジトリです。
AWSのコンソールでECR上にリポジトリを作ってください。名前はecs-handson/nginx
で。
そうするとこのイメージのURIは下記の様になります。(xxxxxxxxxxxの部分は人によって違う)
xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-handson/nginx
このURIのFQDN部分xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
は何度か使うので環境変数にECRのリポジトリを登録しておきます。
ECR_REPOSITORY=xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com
2. ローカルでnginxのイメージを作成する
用意してあるDockerfileを使ってnginxのイメージを作成します。
Dockerfileではnginxのイメージを持ってきてindex.htmlをイメージの中にコピーしています。
cd nginx
docker build -t ${ECR_REPOSITORY}/ecs-handson/nginx:1 ./
ローカルで以下のコマンドで動作するか確認してみましょう。
docker run --rm -p 8081:80 ${ECR_REPOSITORY}/ecs-handson/nginx:1
これでhttp://localhost:8081/にアクセスすると下記の様に表示されるはずです。
3. ECRにログイン
作成したdockerイメージをECRにpushしたいのでまずはECRへdocker login
します。
まずはaws cliを使ってログイン用のパスワードをAWSから取得します。
それをパイプでdockerコマンドへ渡しています。
--profile xxxxx
の部分は使用するAWSのprofileを指定して下さい。
aws ecr --profile xxxxx get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${ECR_REPOSITORY}
Login Succeeded
とでれば成功です。
4. ECRへpush
docker push ${ECR_REPOSITORY}/ecs-handson/nginx:1
問題無くpushできているかAWSの管理コンソールで確認してみて下さい。
5. 必要なIAMロールの作成
AWSで何かするときに必ずついて回るのがロールやポリシーです。細かくてマジ面倒くさいのでAdmin権限つけちゃえーみたいになりますがダメです。数十万、数百万円使われてしまった会社、人が探してみると結構身近にいますよー。
今回使うロールは以下の二つです。
- ECSでつかうEC2インスタンス用
- タスクを実行用
(タスク用のロールもそのうち必要になるけれどいまは要らない)
実は管理コンソールで自動で作成されるのでここは飛ばしてしまっても大丈夫ですが、何やっているのか分かっていた方がいいので手動で作って見ます。
EC2インスタンス用ロール
ロール名は何でもいいのですがecsHandsOnInstanceRole
とつけて、
- AmazonEC2ContainerServiceforEC2Role
- AmazonSSMManagedInstanceCore
の二つのポリシーをつけます。
AmazonEC2ContainerServiceforEC2Role
はEC2インスタンス上で動くECSのエージェントのためのAWSで最初から設定されているポリシーです。
ECRからdockerイメージをpullする、CloudWatchにログを書いたり、ECSのAPIを叩いたり等などのための権限です。こちら を参照の事。
AmazonSSMManagedInstanceCore
はセッションマネージャを使用して管理コンソールからインスタンスへログインするためにつけました。今回は使いませんがあとで立ち上げたEC2インスタンスに入ってみたい場合もあるので。
タスク実行用のロール
EC2インスタンスのロールとあまりやれることは変わりません。おもにFargate用のためにあるのかな・・?よく分からないですがインスタンスロールでダメな理由がそれくらいしか思いつかないです。
ドキュメントにはEC2からもこちらのロールを使ってECRへアクセスしていると書いてあります。
ロール名は何でもいいのですがecsHandsOnTaskExecutionRole
とつけて、
- AmazonECSTaskExecutionRolePolicy
のポリシーをつけて下さい。ECRからpullする、CloudWatchにログを書くだけのポリシーです。インスタンスロールとかぶってます。
6. ECSクラスタ作成
ECSのClusters
のページで
-
Create Cluster
のボタンを押して -
EC2 Linux + Networking
を選択してNext step
- 下記の項目だけ入力
-
Cluster name
をとりあえずnginx
にする(なんでもいい) -
EC2 instance type
はt3.micro
-
Container instance IAM role
は 上で作成したecsHandsOnInstanceRole
を選択(作っていない人はCreate new role
のまま)
-
-
Create
ボタンを押す
3分くらい待つとクラスターが出来てしまいます。裏でCloudFormationが動いてVPCなんかを作っているので確認してみて下さい。
7. タスク定義作成
タスクはnginxをwebサーバーとして立ち上げるだけです。色々な設定がありますが立ち上げるだけならほんの少しのステップで完了です。
ECSのTask Definitions
のページで
-
Create new Task Definitions
のボタンを押して -
EC2
を選択してNext step
- 下記の項目だけ入力
-
Task Definition Name
をとりあえずnginx-task
にする(なんでもいい) -
Task execution role
を先ほどつくったecsHandsOnTaskExecutionRole
にする。 -
Add Container
ボタンをおして-
Container name
をnginx
-
Image
をECRにプッシュしたイメージ名。xxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-handson/nginx:1
-
Memory Limits
を128 -
Port mappings
でAdd port mapping
して80と80を入れます。
-
-
-
Create
を押す
これでできあがりです。
8. サービス作成
サービスを作成してタスクを立ち上げてみます。
ECSのClusters
のページで
-
nginx
クラスタを選択 -
Services
タブでCreate
-
Launch type
をEC2
-
Task Definition
が先ほど作ったタスク定義名nginx-task:1(latest)
になっているのを確認。 -
Service name
はnginx
-
Number of tasks
は1
-
Service type
をDAEMON
-
Next step
->Next step
->Next step
->Create
でサービスが立ち上がります。DAEMONってなんだ?とか色々ありますがまずは動かしてみましょう。
(タスクのRevisionが9になっていますが1と思って下さい)
9.動作確認
問題が無ければ画像の様になっています。
赤丸したところをクリックすると下記の様な画面になります。
ここに書いてあるPublic IP
へアクセスしてみて先ほどローカルで確認したものと同じ画面が表示されれば成功です。
10. index.htmlを更新してみよう
index.htmlの中身を変更してイメージを作ってECRへpushします。
pwd
/Users/xxxx/xxxx/ecs/nginx
echo "nginx test2" > index.html
docker build -t ${ECR_REPOSITORY}/ecs-handson/nginx:2 ./
docker push ${ECR_REPOSITORY}/ecs-handson/nginx:2
これでindex.html
の中身がnginx test2
のコンテナが出来てECRへ登録されたはずです。
11. 新しいタスク定義を作る
まずはイメージがecs-handson/nginx:2
のタスク定義を作ります。
先ほど作ったタスク定義nignx-task
を選択してCreate new revision
をクリックします。
そうすると選択したタスク定義のリビジョンがコピーされた状態でタスク定義を作れるのでConteiner Definitions
のnignx
をひらいてImage
をxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-handson/nginx:2
にしてUpdate
->Create
そうすると新しいタスク定義nginx-task:2
ができます。
12. サービスの更新
EC2インスタンス上で動いているコンテナを差し替えます。
nginx
クラスターを開いてnginx
サービスを選択してUpdate
そうすると先ほどサービスを作った画面を同じような画面がでるので
-
Task Definition
のRevision
を2(latest)
-
Next step
->Next step
->Next step
->Update Service
これだけです。先ほど確認したURLをチェックしてみて下さい。nginx test2
と表示されていれば成功です。
同じようにサービスを更新してRevision
を1
に戻してみるとコンテナが切り替わるはずです。
ECSハンズオンまとめ
別にこんなの手作業でもいいじゃないか?
そうです、今回やったことは手作業でさーばー設定した方がはやいです。
ただこれが20個のインスタンスがあった場合を考えてみて下さい。それをAWSの管理コンソールでポチポチやるだけで更新出来る、何かあれば直ぐに元に戻せますよね。
サービスって意味あるの?
確かに今回やったことはサービスなしで手作業でタスクを立ち上げてもできます。サービスがやっていることはそれを少しだけ自動化しているだけです。
ただ、本来ならここに例えばロードバランサが絡んできます。サービスはインスタンスが20個あれば10個ずつロードバランサから切り離して更新してくれたりしてくれます。他にもインスタンスの数を調整してくれたり、あとは大切なのはタスクの数の維持、つまりタスクがこけたら新しいタスクを立ち上げてくれます。
今回はシンプルにECSを使って見てまずは慣れるのが目的でしたのでもっと深掘りするとこいつ使える奴だなと思うはずです。(多分)
CD/CI ハンズオン
gitリポジトリにpushしたら勝手にECS上のコンテンツが更新されるようにしてみます。
- コンテンツを更新
- gitリポジトリにpush
- 新しいdocker imageが作られる
- 新しいタスク定義が作られる
- サービスが更新される
- タスクが更新される
こんな流れです。
CodeCommit上にリポジトリを作成する
管理コンソールでcodeCommitにecs-handson.nginx
という名前でgitリポジトリをつくります。
cloneしたこのローカルリポジトリにcodeCommitのリポジトリを追加します。
下記の様にすれば追加できますが、codeCommitを使うのに一工夫必要です。
ググれば使い方は沢山でてくるのでそちらを参照して下さい。
sshでもhttpsでもどちらでもいいのでとにかくcodeCommitにpushできる状態を作って下さい。
git remote add codecommit ssh://xxxxxxxxxxxxxxxxxxxx/v1/repos/ecs-handson.nginx
CodePipelineの設定
CodePipelineはCodeCommit、CodeBuild、CodeDeployなどを連携させるためのサービスです。
CodePipelineの設定をしながらCodeBuildの設定をしていきます。
1. 基本設定
パイプラインの名前や使用するロールの設定です。
ロールはNew service role
にしておけば大丈夫です。
2. Source stage
ソースコードをどこから持ってくるかの設定になります。
-
Source provider
はAWS CodeCommit
-
Repository name
は先ほど作ったecs-handson.nginx
-
Branch name
はmain
と入力して下さい。(githubはmasterからmainになってました)
その他はそのままで次へ
3. Build stage
ここではビルドに使うサービスの選択と設定をします。
CodeBuildを選ぶのですがそれが何をするのか理解する必要があります。詳細は後述しますがCodebuildでは今回下記のことをします。
- index.htmlを埋め込んだ新しいnginxのdocker imageを作成する。
- ECRへ作成したimageをpushする。
これって、今までローカルで自分でやっていたことですよね。それをCodeBuildのサーバー上でやれるように設定していきます。
実際のアプリケーションでは必要なライブラリを取ってきたりコンパイルしたり、テストしたりとやること盛りだくさんです。例えばここでテストに失敗するとCodeBuildでエラーがでてCodePipelineが止まります。
-
Build provider
はAWS CodeBuild
-
Project name
でCodeBuildのプロジェクトを選択するのですがまだないのでCreate project
をクリック - そうすると別画面が開きます。
下の画像は別ウインドウで開いたCodeBuildのプロジェクト作成の画面です。画像通りに設定していけばいいのですが必ずPrivileged
のチェックは忘れないようにして下さい。これがないとdocker imageのビルドが出来ません。
Buildspec name
はデフォルトだとbuildspec.yml
なのですが今回ファイル名を変更しているので記入して下さい。
CodeBuildのプロジェクトを作成し終わると下記の様になります。
4. Deploy stage
Deploy stageでは何を使ってデプロイするか選びます。ECSへデプロイするのにはCodeDeployを使って色々できるのですが、今回はECSに付いている便利機能をつかってデプロイするのでDeploy provider
はAmazon ECS
を選択して下さい。
そうするとimagedefinitions.json
という名前のファイルに下記の様にタスク定義に設定したコンテナ名とイメージURLを書くだけで勝手に新しいタスク定義を作ってサービスを更新してくれます。
[
{
"name": "nginx",
"imageUri": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-handson/nginx:2"
}
]
これでCodePipelineが作成できます。するとCodePipelineが動くのですがCodeCommit上に何もpushされてないので失敗しますが放っておいて下さい。
CodeBuildのロール設定
CodeBuildを作る時に自動でロールが作られています。ただCodeBuildで作ったdocker imageをECRへpushするための権限が足りません。ですのでこれを付与してあげます。
先ほどcodebuild-nginx-service-role
という名前でロールが作られているのでAmazonEC2ContainerRegistryPowerUser
ポリシーをつけてあげて下さい。
CodeCommitへpush
実は残念ながらもう何も設定する事がないんです。
index.htmlの中身を変更してコミットしてCodeCommitへpushするとそれが反映されるはずです。
その際にECSクラスタのタスク定義がどうなっているかや、CodePipelineがどのように動いているか確認してみて下さい。
CI/CDまとめ
超シンプルな形でCI/CDを実装してみました。テストもないしgithubと連携しているわけでもないです。
ただそれでもpushしたら自動でサーバー上のコンテンツが更新されると結構「おおお?!」ってなりません?
今回はECSを使いましたがCodeDeployを使えばEC2インスタンス単体にデプロイする事も出来ます。
CodeDeploy単体でも使えるのでSSHでログインしてデプロイしたりFTP使ったりしているなら是非導入してみて下さい。ちょっとしたことなら他のサービス使った方が分かりやすいこともあります。使ってみたなかではBitbucket Pipelines
が一番導入のハードルが低かったです。
ソースをgithubにすればPullRequestと連携させてテストが通らないとmerge出来ないみたいなことも出来たりもしますし色々機能があるのでこれを切っ掛けにみんなが幸せになれると嬉しいです。
後片付け
このままにしておくと料金がかかるので作った物は削除して下さい。
ECSのクラスタを削除すればCloudFormationのスタックが削除されるので立ち上げたインスタンスは削除されます。
(おまけ)buildspec.yml解説
CodeBuildでの処理はbuildspec.ymlに書いてあります。(今回はbuildspec_nginx.ymlにリネームしています)ちなみに他のCIのサービスも代替こんな感じでルートディレクトリに決まった名前のファイルで処理を書きます。
色々な機能(キャッシュやパラメーターストアからデータを取ってきたり等)ありますが今回はシンプルな内容になっています。
今回の実装をコメント形式で解説しておくので興味があれば読んで下さい。
正直なところシェルスクリプトがある程度書けないとキツイです。
version: 0.2
phases:
install:
runtime-versions:
docker: 19
pre_build:
# まずは必要な環境変数などをビルドに必要な情報を整えていきます。
commands:
##### dockerリポジトリへのログイン ###################################
# ECRのリポジトリにログイン
- $(aws ecr get-login --no-include-email --region ${AWS_REGION})
# docker hubへログインしたりもできます。
##### 環境変数の定義 ###################################
# APP_NAME, APP_ENV はcodeBuildのデフォルトの環境変数を使います。ドキュメント参照
# AWSのアカウントIDを$CODEBUILD_BUILD_ARNから取得
- OLDIFS=$IFS && IFS=':' && set -- ${CODEBUILD_BUILD_ARN} && AWS_ACCOUNT_ID=$5 && IFS=$OLDIFS
- ROOT_DIR=`pwd`
# ECR上のdocker imageのURI
- ECR_IMAGE_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/ecs-handson/nginx
# gitのコミットハッシュを使ってdockerイメージのURLにつけるタグを作成する
- COMMIT_HASH=`echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-9`
- PROJECT_DIR=${ROOT_DIR}/nginx
# タスク定義を更新するためのファイル。
- IMGDEF_JSON_FILE=${PROJECT_DIR}/imagedefinitions.json
# 環境変数初期値表示
- |
echo ">>> ENVs ->"
echo ">>> AWS_ACCOUNT_ID: ${AWS_ACCOUNT_ID}"
echo ">>> ROOT_DIR: ${ROOT_DIR}"
echo ">>> ECR_IMAGE_URI: ${ECR_IMAGE_URI}"
echo ">>> COMMIT_HASH: ${COMMIT_HASH}"
echo ">>> PROJECT_DIR: ${PROJECT_DIR}"
echo ">>> IMGDEF_JSON_FILE: ${IMGDEF_JSON_FILE}"
build:
commands:
- IMGDEF_JSON=""
- cd ${PROJECT_DIR}; echo ">>> [dir] `pwd`"
##### dockerイメージのビルド ###################################
# イメージをビルドしてECRへpush。その際にタスク定義更新用のimagedefinitions.jsonの中身も作っている。
- |
set -e
IMGDEF_JSON=${IMGDEF_JSON}$(printf '{"name":"%s", "imageUri": "%s"},' nginx ${ECR_IMAGE_URI}:${COMMIT_HASH})
docker build -t ${ECR_IMAGE_URI} ./
docker tag ${ECR_IMAGE_URI} ${ECR_IMAGE_URI}:latest
docker tag ${ECR_IMAGE_URI} ${ECR_IMAGE_URI}:${COMMIT_HASH}
docker push ${ECR_IMAGE_URI}:latest
docker push ${ECR_IMAGE_URI}:${COMMIT_HASH}
##### imagedefinitions.jsonの書き込み ###################################
- IMGDEF_JSON="["${IMGDEF_JSON:0:${#IMGDEF_JSON}-1}"]"
- echo ${IMGDEF_JSON}
- echo ${IMGDEF_JSON} > ${IMGDEF_JSON_FILE}
# deploy stageへ渡すファイルの定義。今回はimagedefinitions.jsonだけ渡せばいいのだけれど大したサイズではないのでディレクトリごt渡してしまっています。
artifacts:
files:
- '**/*'
discard-paths: no
base-directory: ${PROJECT_DIR}