見出し画像

GitHub Actions+AWS SAM で CI/CD を構築してみた

はじめに

はじめまして。 SHIFTでインフラエンジニアをしている kinno です。
今年の5月にSHIFTに入社して早8ヶ月、優秀な方々に囲まれて切磋琢磨しています。

現在、クラウド環境へのCI/CD構築の案件に携わっており、CI/CDの設計・構築をしています。
CI/CDにも色々ありますが、GitHubでソースコードを管理している場合、
GitHub Actions という機能を使って簡単にCI/CDが実現できることを知りました。

そこで、今回は CI/CD の学習目的で、GitHub Actions と AWS SAM を使用して簡単な CI/CD の構築をしてみました。
CI/CD 初心者の方、GitHub で CI/CD をしたい方の参考になれば幸いです。
※ AWSの基礎用語を理解されている方向けの内容となります。

目標

目標は以下の通りです。
CI/CD の学習はもちろんですが、GitHub Actions や AWSクラウド、IaC についての理解も深めていきます。

・GitHub Actions + AWS SAM で CI/CD を構築・実行する

  • ブランチへの push をトリガーに起動する GitHub Actions ワークフローを作成する

  • ワークフローの中で、テストツール(pytest)を使ったコードの単体テストを実行する

  • 単体テストの結果がOKであれば、AWSクラウド上に Cloud Formation を使用してスタック(Lambda、API Gateway)を作成する。

・サービス構成図

用語解説

CI/CDの構築に取りかかる前に、今回使用するサービスと用語について簡単に紹介します。

  • CI/CD
    継続的インテグレーション/継続的デリバリー(又はデプロイ)のこと。
    アプリケーション開発における一連のプロセスを自動化することで、開発から本番リリースを効率的に行う開発手法。

  • GitHub Actions
    GitHub が提供するワークフローエンジン。
    リポジトリやイシューに対する操作をトリガーに、ワークフローを実行でき、GitHub で CI/CD を実現することができる。

  • AWS サーバーレスアプリケーションモデル (AWS SAM)
    サーバーレスアプリケーション構築用のオープンソースフレームワーク。
    CloudFormation の拡張機能であり、AWSリソースをコードで管理することができる。

事前準備

早速、CI/CDの構築に取り掛かります。まずは事前準備からです。

サインアップ

AWS クラウド上にアプリケーションをデプロイするので、
AWS アカウントのサインアップ、
また、GitHub Actions を利用するため、 GitHub アカウントのサインアップをします。
※ サインアップ手順は割愛します。

GitHub リポジトリの作成

GitHub Actions を実行するリポジトリを作成します。
ソースコードを格納するリポジトリなので、プライベートで作成しました。

このリポジトリには、以下のファイルを格納します。

  • GitHub Actions のワークフローファイル

  • AWS SAM のテンプレートおよび設定ファイル

  • アプリケーションのソースコード

  • pytestのテスト用コード

ディレクトリ構成は以下の通りです。

/
├── .github/
│   └── workflows/
│     └── github-actions.yml    # GitHub Actions のワークフローファイル
├── app/
│   └── functions/
│     └── hello_world.py        # アプリケーションのソースコード
├── tests/
│   └── test_hello_world.py     # pytestのテスト用コード
├── requirements.txt            # インストールパッケージ記載ファイル
├── samconfig.toml              # AWS SAM の設定ファイル
└── template.yml                # AWS SAM のテンプレート

アプリケーションのコードの作成

テストおよびデプロイの対象となるアプリケーションのコードを作成します。今回はCI/CDの構築がメインなので、簡単な Lambda関数 のコードを用意しました。使用言語は Python です。

 import json
 
 def lambda_handler(event, context):
 
 
     return {
         'statusCode': 200,
         'body': json.dumps('Hello world!')
     }

テストツールについて

テストツールには、Pythonの単体テストツールのデファクトスタンダードである「pytest」を使用します。
pytestは、Python スクリプトをテストするためのフレームワークであり、
Python コードでテスト用プログラムを記述することができます。
テストコードは以下の通りです。

import os
import sys

sys.path.append(os.environ["REPOSITORY_HOME"] + "/app/functions")
import hello_world

def test_ok ():


    res = hello_world.lambda_handler(1,2)

    status_code = res['statusCode']
    assert status_code == 200

AWSコンソールの作業

GitHub Actions が AWS 認証をするための設定を行います。
GitHub Actions では、OpenID Connect がサポートされており、一時的なアクセストークンを使用してAWS リソースにアクセスすることができます。
(詳細はこちらを参照。)

IDプロバイダーの登録

AWS に GitHub OIDCプロバイダを登録します。
IDプロバイダを登録することで、AWSアカウントとGitHub OIDCプロバイダ間で信頼関係を確立します。

IAM コンソールの [IDプロバイダ] を開き、 [プロバイダを追加] をクリックします。
以下の通りに設定して、[サムプリントを取得] ⇒ [プロバイダを追加] の順にクリックします。

IAMロールの作成

GitHub Actions で使用する IAM ロールを作成していきます。
IAM ロールには信頼ポリシーと、AWSリソースへのアクセス許可ポリシーを設定します。

IAM コンソールの [IDプロバイダ] を開いて、[ロールを作成]をクリックします。

  • 信頼ポリシー
    信頼ポリシーには、ロールの引き受け先として前の手順で登録したIDプロバイダ(GitHubのOIDC)を指定します。
    ポリシーの JSON は以下の通りです。

{
    "Version": "2012-10-17",
      "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::{AWSアカウント名}:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:{GitHubユーザー名}/sample-git-repository:*"
                }
            }
        }
    ]
}

Principal に、前の手順で作成したIDプロバイダのARNを設定します。
また、Condition にStringLike を使用して、事前準備で作成したリポジトリ
「sample-git-repository」からのみ認証を許可するように設定しています。

  • アクセス許可ポリシー
    アクセス許可ポリシーには、GitHub Actions で操作するAWSリソースの許可ポリシーを設定します。
    AWS SAM のデプロイをするために必要な権限を設定していきます。
    詳細は後ほど説明しますが、SAMデプロイにはS3のフルアクセス権限をつけておく必要があります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "cloudformation:*",
                "s3:*",
                "lambda:*",
                "apigateway:*",
                "iam:CreateRole",
                "iam:DeleteRole",
                "iam:GetRole",
                "iam:PassRole",
                "iam:DeleteRolePolicy",
                "iam:GetRolePolicy",
                "iam:AttachRolePolicy",
                "iam:DetachRolePolicy",
                "iam:PutRolePolicy"
            ],
            "Resource": "*"
        }
    ]
}

S3バケットの作成

AWS SAM ではデプロイ時に、アプリケーションのコードを圧縮して S3 にアップロードしています。
前手順でIAMロールにS3へのアクセス権限が必要となるのは、このデプロイ時にS3にアクセスするためです。
事前にアップロード先の S3 を作成しておきます。

ファイルの作成

AWSの設定が完了したところで、CI/CD 構築に必要となるファイルを作成していきます。

GitHub Actionsワークフロー

GitHub Actionsのワークフローファイルを作成します。
このワークフローファイルには、GitHub Actionsで自動化する処理を書いていきます。
ワークフローファイルは、リポジトリの .github/workflows/ 配下に YML 形式で保存します。

name: Deploy-AWS-CloudFormation
 
on:
  push:
    branches:
      - 'develop'
  workflow_dispatch:
 
env: 
  TEMPLATE_FILE: template.yml
  CONFIG_ENV: sample

jobs:
  deploy:
    runs-on: ubuntu-22.04
    permissions: 
      id-token: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Unit-test
        id: unit-test
        continue-on-error: true
        run: |
          pip install -r requirements.txt -q
          export REPOSITORY_HOME=$(pwd)
          cd tests/
          pytest -v --cov --cov-branch --cov-report=html test_hello_world.py
          cd ${REPOSITORY_HOME}

      - name: Setup python
        if: steps.unit-test.outcome == 'success'
        uses: actions/setup-python@v3

      - name: Setup aws-sam
        if: steps.unit-test.outcome == 'success'
        uses: aws-actions/setup-sam@v2

      - name: Configure AWS credentials
        if: steps.unit-test.outcome == 'success'
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          aws-region: ap-northeast-1
          role-to-assume: arn:aws:iam::{AWSアカウント名}:role/github-actions-oidc-depoloy
      - run: aws sts get-caller-identity

      - name: Build & Deploy Cloudformation stacks
        if: steps.unit-test.outcome == 'success'
        run: |
          sam build --use-container --config-env ${CONFIG_ENV} --template-file ${TEMPLATE_FILE} --parameter-overrides CompanyName=shift ProjectName=blog
          sam deploy --config-env ${CONFIG_ENV} --template-file ${TEMPLATE_FILE} --parameter-overrides CompanyName=shift ProjectName=blog

トリガーは develop ブランチへの push と、手動でも実行できるように workflow_dispatchにしています。

ジョブの内容を細かく見ていくと、

    permissions: 
      id-token: write
      contents: read

permission: ではジョブに対して、IDトークンの書き込み権限を割り当てています。
ワークフローがGitHub OIDCのトークンを要求するために必要になります。

      - name: Unit-test
        id: unit-test
        continue-on-error: true
        run: |
          pip install -r requirements.txt -q
          export REPOSITORY_HOME=$(pwd)
          cd tests/
          pytest -v --cov --cov-branch --cov-report=html test_hello_world.py
          cd ${REPOSITORY_HOME}

ステップ「Unit-test」では、pytest を使ってユニットテストを実行します。
ユニットテストの結果を元に後続の処理(デプロイ)を実行するかどうか、条件分岐をしたいので、
id: で、ステップに一意の識別子「unit-test」を割り当てておきます。

      - name: Setup python
        if: steps.unit-test.outcome == 'success'
        uses: actions/setup-python@v3

      - name: Setup aws-sam
        if: steps.unit-test.outcome == 'success'
        uses: aws-actions/setup-sam@v2

次のステップで、python パッケージ、aws-sam パッケージをインストールおよびセットアップします。
if: steps.unit-test.outcome == 'success' でステップ「Unit-test」の結果が正常終了であれば処理を実行するように、
ステップの実行条件を指定しています。

      - name: Configure AWS credentials
        if: steps.unit-test.outcome == 'success'
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          aws-region: ap-northeast-1
          role-to-assume: arn:aws:iam::{AWSアカウント名}:role/github-actions-oidc-depoloy
      - run: aws sts get-caller-identity

AWSの認証部分です。
aws-actions/configure-aws-credentials@v1-node16 では、GitHub OIDCトークンを取得して、
AWSから指定したロール(github-actions-oidc-depoloy)を引き受けています。

      - name: Build & Deploy Cloudformation stacks
        if: steps.unit-test.outcome == 'success'
        run: |
          sam build --use-container --config-env ${CONFIG_ENV} --template-file ${TEMPLATE_FILE} --parameter-overrides CompanyName=shift ProjectName=blog
          sam deploy --config-env ${CONFIG_ENV} --template-file ${TEMPLATE_FILE} --parameter-overrides CompanyName=shift ProjectName=blog

ここまでのステップを経て、最後にAWS SAMのビルド、デプロイを実行します。

AWS SAM テンプレート

AWS SAM テンプレートファイルを作成します。
このファイルには、AWS 上にデプロイする AWS リソース(Lambda と API Gateway)を定義します。
テンプレートは、CloudFormation 構文を拡張した SAM 構文で記述します。
(実際は、SAM構文で記述されたテンプレートを CloudFormation 構文に変換してデプロイされます。)

AWSTemplateFormatVersion: "2010-09-09"

Transform: AWS::Serverless-2016-10-31

Description: Sample-App

Globals:
  Function:
    Runtime: python3.9

Parameters: 
  CompanyName:
    Type: String
    AllowedValues:
      - shift
  ProjectName: 
    Type: String
    AllowedValues:
      - blog

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${CompanyName}-${ProjectName}-sample-function
      Description: Sample-Function
      CodeUri: ./app/functions
      Policies:
        - AWSLambdaBasicExecutionRole
        - Version: 2012-10-17
          Statement:
            - Resource: "*"
              Effect: Allow
              Action:
                - lambda:InvokeFunction
      Handler: hello_world.lambda_handler
      Events:
        AppMethod:
          Type: Api
          Properties:
            RestApiId: !Ref HelloWorldAPI
            Path: /hello_world
            Method: GET
      Tags:
        Name: !Sub ${CompanyName}-${ProjectName}-sample-function
        CompanyName: !Sub ${CompanyName}
        ProjectName: !Sub ${ProjectName}

  HelloWorldAPI:
    Type: AWS::Serverless::Api
    Properties:
      Name: !Sub ${CompanyName}-${ProjectName}-sample-api
      Description: Sample-API
      StageName: stg
      EndpointConfiguration:
        Type: REGIONAL
      Tags:
        Name: !Sub ${CompanyName}-${ProjectName}-sample-api
        CompanyName: !Sub ${CompanyName}
        ProjectName: !Sub ${ProjectName}

AWS SAM設定ファイル

AWS SAM のコマンド実行時に参照される設定ファイル(samconfig.toml)を作成します。

 version = 0.1
 [sample.deploy.parameters]
 stack_name = "sample-app-stack"
 s3_bucket = "shift-kinno-awssam"
 s3_prefix = "sample-app-stack"
 region = "ap-northeast-1"
 confirm_changeset = false
 capabilities = "CAPABILITY_IAM"
 fail_on_empty_changeset = false

CI/CD の実行

長くなってしまいましたが、これにて準備完了です。
実際に CI/CD を動かしてみたいと思います。

ワークフローの実行

developブランチへのプルリクエストを作成して、マージします。

マージが正常に完了しました。
develop ブランチに push されたことをトリガーに、GitHub Actions のワークフローが動いているはずです。

成功していました!
結果の詳細画面では、各ステップの結果を確認することができます。
成功したステップの左側には、チェックマークがついています。

AWSリソースの確認

AWSクラウドにデプロイされた、Lambda 関数と API ゲートウェイを確認します。
想定通り、Lambda関数が作成されていますね。

API ゲートウェイのエンドポイントから Lambda関数にアクセスしてみます。

Lambda関数の処理結果が返ってきました!
デプロイの結果も問題なしです。

ユニットテストが失敗した場合

最後に、ユニットテスト(pytest)の結果が"NG"だった場合、後続の処理がスキップされるかどうか確認しておきます。
Lambda関数の内容を書き換えて、ワークフローを動かします。

ユニットテストの後続のステップが、全てスキップされていますね。

GitHub Actionsの画面で参照できるログから、pytest の結果が「FAILED」となっていることが確認できます。

まとめ

GitHub ActionsとAWS SAMを組み合わせて簡単なCI/CDを作成してみました。

作業工程はそこそこ多かったですが、ワークフローの作成まで済ませてしまえば、コードを作成および修正 ⇒ テスト ⇒ デプロイ を自動で行うことができます。
ファイルを GitHub で管理したかったので、GitHub Actions を使用しましたが、パイプラインに CodePipeline や CodeCommit を使うのもいいかと思います。

CI/CD について単語の意味をなんとなく覚えていただけでしたが、今回の取り組みで、その仕組みが理解できたような気がします。
次は作成した CI/CD を使って、実際にアプリケーション開発をしてみたいです。

参考


執筆者プロフィール:Kinno Marino
文系学部出身の女性エンジニア。新卒で入社した会社で、認証基盤システムの運用・保守業務を数年経験する。
経験値を上げるため転職を決意し、2022年にSHIFTに入社。
現在は、クラウド環境へのCI/CD構築の案件に従事。日々奮闘中。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/