🙆

コンテナ化と継続的デリバリーを用いたデプロイ

2023/07/20に公開

概要

本記事のゴールは、CI/CDパイプラインを作ることである。GitHubレポジトリと連携して、コードがpushされたら、EKSクラスタにデプロイしたアプリが更新される仕組みを開発する。全体図は、以下に記載する。

  1. GitHubでFlaskアプリが管理されている。複数のエンジニアによって、こちらで最新のコードがあげられる。
  2. GitHubレポジトリにコードがpushされると、AWS CodeBuildがトリガーされる。AWS CodeBuildとは、ソースコードをコンパイル、テスト実行、デプロイする継続的インテグレーションサービスである。CodeBuildはpushされたソースコードから、新しいアプリケーションを作り、それをコンテナレジストリにアップロードする。
  3. 次にCodePipelineは、作成されたアプリケーションイメージを自動的にkubernetesクラスタにデプロイする。
  4. 最後に、kubernetesクラスタは、アプリケーションのサービス提供を開始する。

プロジェクトのToDoリスト

以下に記載する。

  • ローカル環境にて、アプリの動作確認を行う
    • まずは、Flaskアプリをローカルで実行する。
    • 次に、そのアプリをローカルでコンテナ化して、アプリケーション実行に必要な環境を理解する。
  • AWSクラウドでアプリを実行する
    • EKSクラスタとIAMロールを作成する。最初に、任意のリージョンにて、AWS CLIを用いてEKSクラスタを作成する。そして、Codebuildがk8s/EKSクラスタにアクセスできるようなIAMロールを作成する。
    • GitHubのアクセストークンを作成する。GitHubアカウントでアクセストークンを作成する。こちらをAWS CodeBuildで使うことで、GitHubからCodeBuildに対して、コードのpushやテストができるようになる。
    • CloudFormationを用いて、CodeBuild / CodePipelineを作成する。作成は、CloudFormationのテンプレを用いて行う。
    • 最後に、手動でビルドを実行する。それにより、アプリケーションのデプロイが可能となる。

ローカル環境にて、アプリの動作確認を行う

事前準備

下記の用意をすること

最後に、デフォルトリージョンをus-east-2に設定して、GitHubレポジトリのディレクトリへ移動する。

aws configure set region us-east-2
# Run this command to see if the AWSCLI is configured correctly. It should return the list of IAM users. 
aws iam list-users

cd cd0157-Server-Deployment-and-Containerization/

Flaskアプリをローカルで実行

アプリケーションのコンテナ化をする前に、下記事項を実施し動作確認する。

  • アプリに必要なパッケージをインストールする。
# Assuming you are in the cd0157-Server-Deployment-and-Containerization/ directory
pip install -r requirements.txt
  • python main.pyを実行して、Flaskサーバを立ち上げる。立ち上げたら、http://127.0.0.1:8080/ にアクセスして、"Healthy" と表示されるか確認する。
  • /authエンドポイントに対してログイン情報をPOSTして、結果として返却されたトークンをTOKENに保存する。これにより、正しくJWTトークンが取得できているか、確認する。
export TOKEN=`curl --data '{"email":"abc@xyz.com","password":"mypwd"}' --header "Content-Type: application/json" -X POST localhost:8080/auth  | jq -r '.token'
echo $TOKEN

以上が、ローカル環境での動作確認となる。

アプリをローカルでコンテナ化し実行

このステップでは、アプリのDocker化を説明する。これにより、アプリをDockerのコンテナで動かすことができる。

  • 環境変数の保存をする。コンテナはローカルホスト環境の変数読み込みができないので、.env_fileを作成する。こちらで、下記内容を保管する。こちらはあくまで、コンテナをローカル環境で動作確認するためである。なので、GitHubにpushする必要はなく、.gitignoreで上げないように注意する。
JWT_SECRET='myjwtsecret'
LOG_LEVEL=DEBUG
  • イメージをビルドする。
docker build -t myimage .
  • コンテナを構築し動かす。コンテナのポート8080とローカルPCの80を接続する。そして、エンドポイントのチェックをする。Healthyと表示されていればOK。
docker run --name myContainer --env-file=.env_file -p 80:8080 myimage
curl --request GET 'http://localhost:80/'
  • 別の /authエンドポイントについては、下記コマンドで動作確認をする。
# Calls the endpoint 'localhost:80/auth' with the email/password as the message body. 
# The return JWT token assigned to the environment variable 'TOKEN' 
export TOKEN=`curl --data '{"email":"abc@xyz.com","password":"WindowsPwd"}' --header "Content-Type: application/json" -X POST localhost:80/auth  | jq -r '.token'`
echo $TOKEN
# Decrypt the token and returns its content
curl --request GET 'http://localhost:80/contents' -H "Authorization: Bearer ${TOKEN}" | jq .

AWSクラウドでアプリを実行する

まず、EKSクラスタを構築する。EKSクラスタとは、kubernetesをクラウド上で利用できるマネージドサービスである。そして、CodeBuildがEKSクラスタにアプリケーションをデプロイするため、必要となるIAMロールを作成する。そして最後に、CodeBuildとCodePipelineを構築する。この時に、GitHubのアクセストークン、CloudFormationテンプレを使用したCodeBuildとCodePipelineの構築を行う。

EKSクラスタとIAMロール作成

まず最初に、EKSクラスタを構築する。構築されると、下記のように表示される。

eksctl create cluster --name simple-jwt-api --nodes=2 --version=1.22 --instance-types=t2.medium --region=us-east-2

次に、CodeBuild用のIAMロールを作成する。まずは、AWSアカウントのidを取得する。

aws sts get-caller-identity --query Account --output text
# Returns the AWS account id similar to 
# 519002666132

下記の通り、trust.jsonファイルを更新する。

{
"Version": "2012-10-17",
"Statement": [
    {
        "Effect": "Allow",
        "Principal": {
            "AWS": "arn:aws:iam::<ACCOUNT_ID>:root"
        },
        "Action": "sts:AssumeRole"
    }
]
}

そして、下記の通り、IAMロールを作成する。

aws iam create-role --role-name UdacityFlaskDeployCBKubectlRole --assume-role-policy-document file://trust.json --output text --query 'Role.Arn'

最後に、作成したロールに対して、IAMポリシーをアタッチする。

aws iam put-role-policy --role-name UdacityFlaskDeployCBKubectlRole --policy-name eks-describe --policy-document file://iam-role-policy.json

アタッチするポリシーには、EKSとSSMに関連した操作権限が記述されている。

{
 "Version": "2012-10-17",
 "Statement": [
     {
         "Effect": "Allow",
         "Action": [
             "eks:Describe*",
           	 "ssm:GetParameters"
         ],
         "Resource": "*"
     }
 ]

EKS RBACを使用してCodeBuildを認証

わかりやすく説明すると、EKSとはおもちゃの箱のようなもの。ここには、いろいろな種類のおもちゃ(アプリケーション)が入っていて、Amazonがその箱を管理している。一方、CodeBuildとは、ロボット工場のようなもの。ここでは、パーツ(コード)が入ってきて、それをロボットが組み立てて新しいおもちゃ(アプリケーション)を作る。

この箱は鍵がかかっていて、誰でもおもちゃに触れるわけではない。鍵を持つ人だけが触れる。それがRBACで、役割に基づいて鍵を持つ人を決める。aws-auth ConfigMapとは、大きな鍵のリストである。ここに名前が書かれた人だけが鍵をもらって、箱の中のおもちゃに触れる。

だから、ロボット工場(CodeBuild)新しいおもちゃを箱に入れたり、既存のおもちゃを修理する時には箱の鍵が必要となる。そのため、鍵のリスト(aws-auth ConfigMap)を書き換える必要がある。そうすると、箱の鍵をもらって、おもちゃを自由に扱える。

この鍵は、新しく箱(EKSクラスタ)を作るたびに、リストを書き加える手続きが必要となる。

以下で、専門用語の説明をする。

  • RBACとは、Role-Based Access Controlのことで、ロールに基づいてシステムへのアクセスを制御する方式。個々のユーザではなく、ユーザの役割に基づいて、アクセス権限を設定する。
  • ConfigMapとは、EKSクラスタへのアクセスを制御するための重要なkubernetesリソースである。このConfigMapはEKSクラスタにIAMユーザやロールを関連づけて、これらに対して特定のkubernetesロールを付与するために使われる。

上記理由より、CodeBuildがEKSクラスタを管理するためには、CodeBuild用のIAMロールをConfigMapに追加する必要がある。これによって、CodeBuildはEKSクラスタ内で操作が可能となる。具体的な手順は以下の通り。

  • CodeBuild用のIAMロールを作成する。こちらには、EKSクラスタで必要な操作を実行するためのアクセス権を付与する。こちらは、既に実施済み。
  • 次に、kube-systemにあるaws-authというConfig Mapを取得して、その結果をYAML形式で表示して、/tmp/aws-auth-patch.ymlにリダイレクトする。つまり、EKSクラスタでConfigMapを取得する。
kubectl get -n kube-system configmap/aws-auth -o yaml > /tmp/aws-auth-patch.yml
  • ConfigMapのYAMLを編集して、新たに作成したIAMロールを追加する。まずaws-auth-patch.ymlファイルを開く
code /System/Volumes/Data/private/tmp/aws-auth-patch.yml
  • そして、下記内容を追加する。
   - groups:
       - system:masters
     rolearn: arn:aws:iam::<ACCOUNT_ID>:role/UdacityFlaskDeployCBKubectlRole
     username: build   

  • 最後に、ConfigMapを更新する。
kubectl patch configmap/aws-auth -n kube-system --patch "$(cat /tmp/aws-auth-patch.yml)"

CodePipelineとCodeBuildを使用したKubernetesへのデプロイ

では、パイプラインを使用して、GitHubのコードpushを監視して、アプリケーションの継続的CI/CDを行う。レポジトリに対してコードがpushされた際に、新しいイメージが作成されて、EKSにデプロイされる。

GitHubトークンの作成

まず最初に、下記リンクからGitHubトークンを作成する。これにより、プライベートリポジトリのアクセス権を付与できる。
https://github.com/settings/tokens/

CloudFormationによるCI/CDパイプライン作成

ForkしたGitHubのレポジトリで、ci-cd-codepipeline.cfn.ymlファイルがあるので、そちらでCloudFormationを実行する。実行手順は、下記を参考にすること。
https://youtu.be/NYqmLUaCUyk

そして、GitHubUserのDefaultがSudKulとなっているので、自身のGitHubユーザ名に書き換える。

パスワードをAWSパラメータストアに保存

EKSクラスタで、暗号情報をJWTとして保管する必要がある。前述の通り、/authのエンドポイントで、myjwtsecretというテキストを使用した。こちらをEKSでも使うため、以下のコードでAWSパラメータストアに保管する。

aws ssm put-parameter --name JWT_SECRET --overwrite --value "myjwtsecret" --type SecureString
## Verify
aws ssm get-parameter --name JWT_SECRET

buildspec.ymlの設定

こちらは、AWS CodeBuildとEKSクラスタを使うとき、それらがお互いうまく働くようにするための指示である。

  • 最初に、どのバージョンの道具(kubectl)を使っているか、確認する。kubectl version --short --clientを実行することで、確認できる。
  • 次に、AWSもしくはKubernetesドキュメントを参照して、Linuxマシン用の利用可能なバージョンを調べる。調べた内容に基づいて、buildspec.ymlのkubectlのバージョンを変更する。以下は一例である。
- KUBECTL_VERSION="v1.21.2"  # Your chosen version
  • 最後に、秘密(JWT_SECRET)を、CodeBuildが使えるようにする。これは、buildspec.ymlの一番最後に記載する必要がある。
env:
  parameter-store:         
    JWT_SECRET: JWT_SECRET

CI/CDパイプラインの完成

以下の通り、パイプラインが作られる。
https://youtu.be/87EGNJSKYgA

以下の手順でコードをコミットすると、パイプラインが実行される。

## Verify the remote destination. 
## It should point to the repo in your account (not the repo in the Udacity account). 
## Otherwise, FORK the Udacity repo, and then clone it locally
git remote -v
## Make the changes locally
git status
## Add the changed file to the Git staging area
git add <filename>
## Provide a meaningful commit description
git commit -m “my comment“
## Push to the local master branch to the remote master branch
git push

そうすると、buildが実行されたことを確認できるはずである。

APIエンドポイントの動作確認

最後に、外部IPアドレスを使って、APIエンドポイントの動作確認を行う。まずは、下記コマンドでサービスの外部IPアドレスを取得する。

kubectl get services simple-jwt-api -o wide

このコマンドは、simple-jwt-apiというサービス情報を取得し、external-ipを取得できる。

次に、その外部IPアドレスを使って、正しくAPIエンドポイントが動作しているかテストする。実行スクリプトは、以下の通りである。

export TOKEN=`curl -d '{"email":"<EMAIL>","password":"<PASSWORD>"}' -H "Content-Type: application/json" -X POST <EXTERNAL-IP URL>/auth  | jq -r '.token'`
curl --request GET '<EXTERNAL-IP URL>/contents' -H "Authorization: Bearer ${TOKEN}" | jq

まずauthエンドポイントにPOSTリクエストを送って、トークンを取得する。そのトークンを使って、contentsエンドポイントにGETリクエストを送信して、${TOKEN}という環境変数に保存する。

ビルドプロセスにテストを追加

新しいコードをクラスタにデプロイする前に、ユニットテストが通過することを要求する。それで通過した場合のみ、クラスタに新しいコードをデプロイする。

  • まずbuildspec.ymlファイルを開く。pre_buildセクションで、必要なソフトウェアをインストールする。
pre_build:
  commands:
    - pip3 install -r requirements.txt 
    - python3 -m pytest test_main.py
  • 次に、test_main.pyファイルを開く。任意のテストで、assert Falseを追加することで、故意にテストを失敗させる。その結果、不適切なデプロイが防止されるか、下記のように確認できる。

まとめ

今回は、CI/CDパイプラインを使用した、アプリケーションのデプロイ方法を記載した。

  • まずは、ローカルでアプリケーションの動作確認をした。各エンドポイントが、正しく実行されているか、確認した。
  • 次に、アプリケーションのコンテナ化を行った。その上で、コンテナとローカルPCのポートを繋いで、動作確認をした。
  • アプリケーションをデプロイするため、EKSクラスタ / CodeBuild向けのIAMロール / CodeBuildを作成した。CodeBuildが適切にEKSクラスタを使用できるように、EKS RBACの認証設定をした。
  • 最後に、CodePipelineとCodeBuildへアプリケーションをデプロイした。これにより、GitHubでコード更新がされたら、適宜アプリケーションが更新される。
    • そのために、まずGitHubアクセストークンを発行した。これにより、AWSサービスがGitHubにアクセスできる。
    • 次に、CodeBuildとCodePipelineをCloudFormationを使って作成した。

上記内容で、コードをGitHubにpushすると、アプリケーションが更新されるのを確認できるはずである。

参照

https://learn.udacity.com/nanodegrees/nd0044/parts/cd0157/lessons/02711da3-9cc6-480f-a124-dfdb2a0aecdd/concepts/a045d0a1-c7f4-489e-9c31-12637b82ab9a

Discussion