LoginSignup
5
7

More than 3 years have passed since last update.

ECSとCI/CD入門

Last updated at Posted at 2020-12-23

昔は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台ずつとか全部まとめてとか色々。
    • サービスで動かせるタスクの種類は一つ。
  • クラスタ
    • サービスとタスク、インスタンスなどのカタマリ
    • 一番大きな集合

箱入りの饅頭とか大福を想像するとちょっと分かりやすいかも?

  • タスク ➡️ あんこ
  • サービス ➡️ 皮
  • クラスタ ➡️ 箱

sweets_manju_onsen.png

サービスなしで手動でタスクだけをクラスタ上で動かすことも出来ますが混乱するので気にしない方がいいです。(自分はこいつのせいでサービスとタスクの関係がなんなのか理解しにくかったです)

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/にアクセスすると下記の様に表示されるはずです。

スクリーンショット 2020-12-22 14.46.48.png
問題無く表示されていたらctrl+cでコンテナを停めます。

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の管理コンソールで確認してみて下さい。

スクリーンショット 2020-12-22 14.57.07.png

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のページで

  1. Create Clusterのボタンを押して
  2. EC2 Linux + Networkingを選択してNext step
  3. 下記の項目だけ入力
    • Cluster nameをとりあえずnginxにする(なんでもいい)
    • EC2 instance typet3.micro
    • Container instance IAM roleは 上で作成したecsHandsOnInstanceRoleを選択(作っていない人はCreate new roleのまま)
  4. Createボタンを押す

3分くらい待つとクラスターが出来てしまいます。裏でCloudFormationが動いてVPCなんかを作っているので確認してみて下さい。

7. タスク定義作成

タスクはnginxをwebサーバーとして立ち上げるだけです。色々な設定がありますが立ち上げるだけならほんの少しのステップで完了です。

ECSのTask Definitionsのページで

  1. Create new Task Definitionsのボタンを押して
  2. EC2を選択してNext step
  3. 下記の項目だけ入力
    • Task Definition Nameをとりあえずnginx-taskにする(なんでもいい)
    • Task execution roleを先ほどつくったecsHandsOnTaskExecutionRoleにする。
    • Add Containerボタンをおして
      • Container namenginx
      • ImageをECRにプッシュしたイメージ名。xxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-handson/nginx:1
      • Memory Limitsを128
      • Port mappingsAdd port mappingして80と80を入れます。
  4. Createを押す

これでできあがりです。

8. サービス作成

サービスを作成してタスクを立ち上げてみます。

ECSのClustersのページで

  1. nginxクラスタを選択
  2. ServicesタブでCreate
  3. Launch typeEC2
  4. Task Definitionが先ほど作ったタスク定義名nginx-task:1(latest)になっているのを確認。
  5. Service namenginx
  6. Number of tasks1
  7. Service typeDAEMON
  8. Next step -> Next step -> Next step -> Create

でサービスが立ち上がります。DAEMONってなんだ?とか色々ありますがまずは動かしてみましょう。

スクリーンショット 2020-12-22 19.09.10.png
(タスクのRevisionが9になっていますが1と思って下さい)

9.動作確認

問題が無ければ画像の様になっています。

スクリーンショット 2020-12-22 19.15.13.png

スクリーンショット 2020-12-22 19.15.36.png

赤丸したところをクリックすると下記の様な画面になります。

スクリーンショット 2020-12-22 19.19.56.png

ここに書いてあるPublic IPへアクセスしてみて先ほどローカルで確認したものと同じ画面が表示されれば成功です。

スクリーンショット 2020-12-22 19.22.26.png

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をクリックします。
スクリーンショット 2020-12-22 19.52.17.png

そうすると選択したタスク定義のリビジョンがコピーされた状態でタスク定義を作れるのでConteiner DefinitionsnignxをひらいてImagexxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-handson/nginx:2にしてUpdate->Create

スクリーンショット 2020-12-22 19.54.36.png

そうすると新しいタスク定義nginx-task:2ができます。

12. サービスの更新

EC2インスタンス上で動いているコンテナを差し替えます。
nginxクラスターを開いてnginxサービスを選択してUpdate
スクリーンショット 2020-12-22 20.02.13.png

そうすると先ほどサービスを作った画面を同じような画面がでるので

  1. Task DefinitionRevision2(latest)
  2. Next step -> Next step -> Next step -> Update Service

これだけです。先ほど確認したURLをチェックしてみて下さい。nginx test2と表示されていれば成功です。

スクリーンショット 2020-12-22 20.06.32.png

同じようにサービスを更新してRevision1に戻してみるとコンテナが切り替わるはずです。

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にしておけば大丈夫です。

スクリーンショット 2020-12-22 20.33.20.png

2. Source stage

ソースコードをどこから持ってくるかの設定になります。

  • Source providerAWS CodeCommit
  • Repository nameは先ほど作ったecs-handson.nginx
  • Branch namemainと入力して下さい。(githubはmasterからmainになってました)

その他はそのままで次へ

スクリーンショット 2020-12-22 20.33.41.png

3. Build stage

ここではビルドに使うサービスの選択と設定をします。
CodeBuildを選ぶのですがそれが何をするのか理解する必要があります。詳細は後述しますがCodebuildでは今回下記のことをします。

  • index.htmlを埋め込んだ新しいnginxのdocker imageを作成する。
  • ECRへ作成したimageをpushする。

これって、今までローカルで自分でやっていたことですよね。それをCodeBuildのサーバー上でやれるように設定していきます。
実際のアプリケーションでは必要なライブラリを取ってきたりコンパイルしたり、テストしたりとやること盛りだくさんです。例えばここでテストに失敗するとCodeBuildでエラーがでてCodePipelineが止まります。

  • Build providerAWS CodeBuild
  • Project nameでCodeBuildのプロジェクトを選択するのですがまだないのでCreate projectをクリック
  • そうすると別画面が開きます。 スクリーンショット 2020-12-22 20.34.43.png

下の画像は別ウインドウで開いたCodeBuildのプロジェクト作成の画面です。画像通りに設定していけばいいのですが必ずPrivilegedのチェックは忘れないようにして下さい。これがないとdocker imageのビルドが出来ません。
スクリーンショット 2020-12-22 20.36.27.png
スクリーンショット 2020-12-22 20.36.44.png
スクリーンショット 2020-12-22 20.37.36.png

Buildspec nameはデフォルトだとbuildspec.ymlなのですが今回ファイル名を変更しているので記入して下さい。


CodeBuildのプロジェクトを作成し終わると下記の様になります。
スクリーンショット 2020-12-22 20.39.31.png

4. Deploy stage

Deploy stageでは何を使ってデプロイするか選びます。ECSへデプロイするのにはCodeDeployを使って色々できるのですが、今回はECSに付いている便利機能をつかってデプロイするのでDeploy providerAmazon ECSを選択して下さい。
そうするとimagedefinitions.jsonという名前のファイルに下記の様にタスク定義に設定したコンテナ名とイメージURLを書くだけで勝手に新しいタスク定義を作ってサービスを更新してくれます。

[
  {
    "name": "nginx",
    "imageUri": "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/ecs-handson/nginx:2"
  }
]

スクリーンショット 2020-12-22 20.40.07.png

これで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}
5
7
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
7