こんにちは、IaC技術推進部の松尾です。
GitHub ActionsでCI/CD環境を構築してみたので記事を書いてみます。躓いたポイントなど参考になれば。
構築の目的
CI/CDを理解する為というのが一番の目的です。覚えるには実際に手を動かしてみるのが一番かなと。
構築の手段
構築手順は、弊社が提供予定の「CI/CD体験パッケージ」の手順を利用していきます。 「CI/CD体験パッケージ」は、簡易的にCI/CD体験が出来る一連の手順を纏めたものになります。(来年公開予定) 「CI/CD体験パッケージ」の内容としては、バージョン管理にGitHub、CI/CD機能にGitHubActions、環境にAWS、を利用するものになります。
CI/CD体験パッケージのファイル構成
CI/CD体験パッケージのファイル、フォルダ構成はこのようになっています。 大まかに、AWSへリソースを構築する為のCloudFormation用ファイル、GitHub Action用のワークフローファイル、サンプルWebアプリのファイル、から構成されています。
このファイル一式をGitHubリポジトリへコミットした状態から構築開始していきます。
C:. │ .gitignore │ app.py │ Dockerfile │ README.md │ requirements.txt │ task-definition.json ---------------------------ワークフロー「aws-ecs-deploy.yml」でECSをデプロイする際に参照されるタスク定義を記載したファイル。 │ ├─.github │ └─workflows---------------------------------.github/workflws/*へ配置されたファイルがGitHubワークフローファイルとして読み込まれる │ aws-cloudformation-deploy.yml-------ステップ1でAWSリソースを作成する為のyamlファイル。 │ aws-ecs-deploy.yml--------------------ステップ2とステップ3でECSをデプロイする為のyamlファイル。 │ ├─app------------------------------------------サンプルWebアプリ用フォルダ │ │ __init__.py │ │ │ ├─models │ │ model.py │ │ │ ├─static │ │ ├─css │ │ │ style.css │ │ │ │ │ └─images │ │ books.jpg │ │ favicon.ico │ │ │ ├─templates │ │ index.html---------------------------ステップ3でVersion2のWebアプリに更新する際に修正するファイル。 │ │ layout.html │ │ resigter_articles.html │ │ resigter_tags.html │ │ update_articles.html │ │ │ └─views │ articles.py │ tags.py │ ├─cloudformation-------------------------CloudFormation用フォルダ。 │ │ alb.yml--------------------------------aws-cloudformation-deploy.ymlが参照するファイル。 │ │ ec2.yml-------------------------------同上 │ │ ecr.yml-------------------------------同上 │ │ ecs.yml-------------------------------同上 │ │ rds.yml-------------------------------同上 │ │ securitygroup.yml-------------------------------同上 │ │ vpc.yml-------------------------------同上 │ │ vpcendpoints.yml-------------------------------同上 │ │ │ └─manual----------------------------手動実行用のCloudFormation用フォルダ。 │ iam.yml--------------------------GitHub ActionがAWSへ接続する為の認証情報を作成するファイル。 │ secretsmanager.yml-------------サンプルWebアプリがDBへ接続する為の認証情報を作成するファイル。 │ ├─docs │ │ ApplicationDesign.md │ │ CICDFlowDesign.md │ │ StartupGuide.md------------------構築手順ガイド。 │ │ │ └─images │ architecture.svg │ cicd_architecture.svg │
概要図
まずはゴールを考えてみます。完成イメージはこんな感じでしょうか。実際はECSをデプロイする為にAWSへVPCやサブネット、SGなども作りますが図には記載していません。 「Git Users」のcommitをトリガーとして、PushされたImageを元に、ECSへ自動でデプロイされる仕組みを構築していきます。
構築準備
バージョン管理ツールとCI/CDツールがあれば構築できます。今回はGitHubとGitHubActionsになります。 環境はAWSを使っていきます。
GitHubアカウントの用意
今回、バージョン管理はGitHubを利用します。フリープランで問題無いので用意します。CI/CDツールとしてGitHub Actionsも使います。
AWSアカウントの用意
CI/CD環境をAWS上に構築していきます。AWSアカウントとIAMユーザを作成出来る権限が必要です。(今回はアクセスキーとシークレットキーを利用)
AWS CloudFormationの知識(Better)
CI/CD環境のベースとなるAWS上のリソースをClodFormationで構築していきます。AWSリソースの作成そのものは、CI/CDの本筋ではないので極力自動化して作っていきます。
構築手順概要
ステップ1~3として大きく3段階で進めていきます。
ステップ1はAWS環境の構築です。CI/CDの本筋ではないのでCloudFormationでさくっと作ってしまいます。
ステップ2では、初回の構築を想定したWebアプリをECSへデプロイしていきます。
ステップ3では、更新を加えたWebアプリをバージョン2としてcommitし、ECSへのデプロイまで自動で展開される過程を見ていきます。
ステップ1. AWS環境構築
AWSへGitHubActions用のCredentialを作成
AWSへGitHubActions用の踏み台サーバとキーペアを作成
GitHubActionsワークフローファイルを編集しAWSリソースを作成
Webアプリ用のデータベースを作成
ステップ2. Webアプリを構築
GitHubActions用のタスク定義ファイルを作成
Webアプリをデプロイ
ステップ3. Webアプリを更新
更新したWebアプリをコミットすると自動でデプロイされることを確認
構築
では進めていきます。
ステップ1. AWS環境構築
AWSへGitHubActions用のCredentialを作成
CI/CDの準備部分でAWS側のみの作業となるため、構築手順は割愛します。
AWSへGitHubActions用の踏み台サーバとキーペアを作成
CI/CDの準備部分でAWS側のみの作業となるため、構築手順は割愛します。
GitHubActionsワークフローファイルを編集しAWSリソースを作成
2つのワークフローファイルを編集していきます。
.github/workflow/aws-cloudformation-deploy.yml ・・・ステップ1でAWSリソースを作成する為のyamlファイル。AWSへClodFormationで複数リソースを作成することが目的のため説明は割愛。
.github/workflow/aws-ecs-deploy.yml ・・・ステップ2とステップ3でECSをデプロイする為のyamlファイル。初回のECSデプロイ、2回目以降のECSデプロイでも使用されるyaml。 元となるファイルは一般公開されているもの「
Amazon Elastic Container Serviceへのデプロイ - GitHub Docs
」 です。
元となるファイルからの変更箇所(★)と、設定の一部について説明をしていきます。
.github/workflow/aws-ecs-deploy.yml
name: Deploy to Amazon ECS on:-------------------------------ワークフローファイル実行のトリガー設定 push:---------------------------トリガー設定を「push」とするとGithubにpushされることがトリガーとなる。 branches: [ "main" ]----------「branches」を「main」とすると、ブランチがmainの条件にマッチした場合がトリガー条件となる。 paths-ignore:-----------------★ただし、「paths-ignore」で定義したパスにマッチするpushの場合は無視する。 - '.github/**' - 'README.md' - 'cloudformation/**' - 'docs/**' - 'helloworld/**' env:-----------------------------★ECSのbuild、deployに必要な変数を定義 AWS_REGION: ap-northeast-1 # set this to your preferred AWS region, e.g. us-west-1 ECR_REPOSITORY: m0j1-knowledge-flask # set this to your Amazon ECR repository name PJ_NAME: m0j1-knowledge-flask ECS_TASK_DEFINITION: task-definition.json # set this to the path to your Amazon ECS task definition # file, e.g. .aws/task-definition.json # containerDefinitions section of your task definition --------------------------------以下設定はデフォルト値。BuildしたイメージをECRへプッシュ、トリガーにマッチした場合にECSへイメージをDeployする動作を行う。詳細は元ファイルのリンク参照。 permissions: contents: read jobs: deploy: name: Deploy runs-on: ubuntu-latest environment: production steps: - name: Checkout uses: actions/checkout@v3 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ${{ env.AWS_REGION }} - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push image to Amazon ECR id: build-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} IMAGE_TAG: ${{ github.sha }} run: | # Build a docker container and # push it to ECR so that it can # be deployed to ECS. docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" - name: Fill in the new image ID in the Amazon ECS task definition id: task-def uses: aws-actions/amazon-ecs-render-task-definition@v1 with: task-definition: ${{ env.ECS_TASK_DEFINITION }} container-name: ${{ env.ECR_REPOSITORY }} image: ${{ steps.build-image.outputs.image }} - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: ${{ steps.task-def.outputs.task-definition }} service: ${{ env.PJ_NAME }}-ecs-service cluster: ${{ env.PJ_NAME }}-ecs-cluster wait-for-service-stability: true
Webアプリ用のデータベースを作成
CI/CDの準備部分でAWS側のみの作業となるため、構築手順は割愛します。
ステップ1の完了後はAWS上にECSが展開された状態になっています。具体的にはVPC、サブネット、セキュリティグループ、VPCエンドポイント、RDS、ALB、ECR、ECSが構築された状態です。(赤枠内が構築された)
ステップ1の構築完了確認として、ALBのDNS名へアクセスしてみます。
無事HelloWorldが表示されました。これでクライアントからECS上のアプリまで疎通出来ることが確認出来たことになります。
構成図でいうとこの部分の疎通確認ですね。
ステップ2. AWS ECS上にWebアプリを構築
ステップ2の完了後イメージは、ECSへWebアプリ(バージョン1)がデプロイされた状態です。
GitHubActions用のタスク定義ファイルを作成
GitHubActionsでECSをデプロイするためのタスク定義ファイル(task-definition.json)を編集していきます。
familyブロック
familyのパラメータを先ほど構築したECSのタスク名に修正します。
項目 | 値 |
---|---|
family | デプロイするECSのタスク名 |
Before
{ "family": "knowledge-flask-ecs-service-task", "networkMode": "awsvpc", "containerDefinitions": [ {
After
{ "family": "m0j1-knowledge-flask-ecs-service-task", "networkMode": "awsvpc", "containerDefinitions": [ {
containerDefinitionsブロック
コンテナ定義のパラメータを各リソース名に修正します。
項目 | 値 |
---|---|
containerDefinitions[0].name | ECSのデプロイ元となるイメージを配置するECR名 |
containerDefinitions[0].image | ECSのデプロイ元となるイメージ*1 |
containerDefinitions[0].environment[0].value | RDSに作成したDBのエンドポイント名 |
containerDefinitions[0].secrets[0].valueFrom | SecretManagerに登録したusernameのARN |
containerDefinitions[0].secrets[1].valueFrom | SecretManagerに登録したpasswordのARN |
containerDefinitions[0].logConfiguration.options.awslogs-group | ECSログの配置パス |
containerDefinitions[0].logConfiguration.options.awslogs-region | ECSログの保存リージョン |
*1・・・このimage名はpushした際のECRのimage名に置き換わるのでダミーのパラメータになります。 imageURIを置き換える処理はAWSが提供する「AWS for GitHub Actions」の「amazon-ecs-render-task-definition」というActionが行います。
Before
"containerDefinitions": [ { "name": "knowledge-flask", "image": "knowledge-flask", "cpu": 256, "memory": 512, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ], "environment": [ { "name": "DATABASE_ENDPOINT", "value": "knowledge-flask-postgres.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com" }, { "name": "DATABASE_NAME", "value": "knowledge" } ], "secrets": [ { "name": "DATABASE_USERNAME", "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:000000000000:secret:postgresql-credential-XXXXXX:username::" }, { "name": "DATABASE_PASSWORD", "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:000000000000:secret:postgresql-credential-XXXXXX:password::" } ], "essential": true, "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/knowledge-flask-ecs-service", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "ecs", "awslogs-create-group": "true" } } } ], "runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" }, "requiresCompatibilities": [ "FARGATE" ],
After
AWSアカウントIDなど一部マスク(******)していますが実際に構築した値を入れていきます。
"containerDefinitions": [ { "name": "m0j1-knowledge-flask", "image": "hoge", "cpu": 256, "memory": 512, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ], "environment": [ { "name": "DATABASE_ENDPOINT", "value": "m0j1-knowledge-flask-postgres.************.ap-northeast-1.rds.amazonaws.com" }, { "name": "DATABASE_NAME", "value": "knowledge" } ], "secrets": [ { "name": "DATABASE_USERNAME", "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:************:secret:postgresql-credential-******:username::" }, { "name": "DATABASE_PASSWORD", "valueFrom": "arn:aws:secretsmanager:ap-northeast-1:************:secret:postgresql-credential-******:password::" } ], "essential": true, "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/m0j1-knowledge-flask-ecs-service", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "ecs", "awslogs-create-group": "true" } } } ], "runtimePlatform": { "cpuArchitecture": "X86_64", "operatingSystemFamily": "LINUX" }, "requiresCompatibilities": [ "FARGATE" ],
executionRoleArnブロック
実行ロールのARNを記載していきます。
項目 | 値 |
---|---|
executionRoleArn | ECS実行用に作成したRole |
Before
"executionRoleArn": "arn:aws:iam::000000000000:role/knowledge-flask-ecs-cluster-task-execution-role", "cpu": "256", "memory": "512" }
After
"executionRoleArn": "arn:aws:iam::************:role/m0j1-knowledge-flask-ecs-cluster-task-execution-role", "cpu": "256", "memory": "512" }
これでタスク定義ファイルの準備が整いました。commitしてアプリがデプロイされることを見ていきます。
Webアプリをデプロイ
git push 後にGitHubActionsを見てみると、push をトリガーとしてWorkflowが動いていることが確認出来ました。完了まで暫く待ってみます。
成功しました!
ブラウザから先ほどと同じURLへアクセスしてみると
想定通りのWebアプリが表示されました。これは一回目のBuildとDeployがされた状態(Version1のWebアプリデプロイを疑似した状態)です。
ステップ3. Webアプリを更新
ステップ3の完了後イメージは、ECSへWebアプリ(Version2)がデプロイされた状態です。
更新したWebアプリをコミットすると自動でデプロイされることを確認
CI/CDの流れを確認する為、Webアプリを更新してcommitしてみます。 これにより、Version2のWebアプリデプロイを疑似しています。
更新箇所は、トップページとなる .app/templates/index.html へVerison2! を追加しました。
再度commitすると、自動的にワークフローが開始され、
「App Version 2 commit 」が完了します。
ブラウザを見てみます。
想定通り、修正したWebアプリ(Version2!の追記)がデプロイされています。
今回のCI/CD構築はここまで。
まとめ
構築前はCI/CDの具体的なイメージがし辛かったのですが、実際に構築してみると一連の流れが分かりやすく、理解が進んだなという印象です。 この構築は通して2回行ったのですが、2回目の理解度がだいぶ違いました。
また、今回はCI/CDの「Code」⇒「Build」⇒「Test」⇒「Deploy」のうち、「Test」部分が無い構成でした。 実案件では「Test」部分も重要な要素かと思うので確認しておく必要があるなと感じました。
「Deploy」方法についても、今回は一度に切り替えてますが、実際には切り替え方法も 環境に合ったベストな形を選択していくことが必要だと思いました。 具体的には、段階的な切り替え、全体が切り替わる時間、切り戻しのリスク/しやすさなどでしょうか。
CI/CDツールも、オンプレとクラウドに多数の製品があるので、メリデメを踏まえた提案をしていきたいなと思います。
弊社ではCI/CDの導入支援サービスも行っています。 是非ご検討下さい。
(おまけ)デプロイの失敗
ステップ2. Webアプリを構築
Webアプリをデプロイ
を実行した際、Failする場合がありました。原因はパラメータの記述に良くない点があったことで、対策としては以下のように修正することでOKでした。
Webアプリをデプロイ時、Workflowは完了しましたがFail..
ログを追っていくとwarningメッセージが出ていました。
The
set-output
command is deprecated and will be disabled soon. Please upgrade to using Environment Files. For more information see: https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/
リンク先を見ると「set-output」が廃止されるようです。
リンク先の説明を参考に、今回のパラメータでは、.github/workflow/aws-ecs-deploy.ymlのこの部分を
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
このように修正すればOK。
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
commitして成功でした。