0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GithubActionsでEC2への自動デプロイ(APIGateway&Lambda構成)

Last updated at Posted at 2025-01-26

背景

Reactアプリケーションの機能追加/改修などのたびにmainブランチにpushする。
そのあと、Apacheサーバーへ接続してpullを行いApacheサーバー上でビルドをするという完全手動のデプロイを行っていた。
これを完全自動化したい。

目的

  1. Github Actionsの概要を理解する。(YAML/料金)
  2. ビルド&デプロイを自動化できることを確認する

イメージ

image.png

本来は
・③、④でGithubActionsからのwebhookであることの認証やIAMを用いた正当性の確認
・APIGateway間とのHTTPS化
・プライベートなんだからインターネットゲートウェイじゃなくてNATだろ...
などツッコミどころありますが、許して下さい(あくまで検証なので)

手順

Github Actionsの導入

Github ActionsはGitリポジトリ配下にYAMLファイルを設置して実行する。
このYAMLファイルにワークフローを定義する。
ワークフロー内部にYAML記法を用いて以下を定義する。
・どのタイミングで実行するか
・前処理(仮想サーバーなら各種インストールなど)
・ジョブ(テスト・ビルド・デプロイ)
・タイムアウト時間

まずは、該当リポジトリのGithubリポジトリに移動し、『▶Actions』をクリックする。
image.png

リポジトリの特性に応じて、github側がワークフローを提供してくれています。
が、今回は自分で書いていくので『set up a workflow yourself』をクリックします。
image.png

『リポジトリ名/.github/workflows』直下に『ブランチ名.yml』が作成されます。
ファイル内の記述は後で行うとして一旦ファイル作成までをコミットします。

『Commit changes』をクリックします。
image.png

今回は検証ですのでコミットメッセージ&ディスクリプションは適当でよいでしょう。
また、コミットするブランチもmainに直接で良いでしょう。(本来は別ブランチ切ってプルリクはさむのが正しい気がしますが...)
image.png

YAMLファイルにワークフローを定義する

次は、実際にymlファイルにCI/CDの内容を記載していきます。

Github Actionsの構文に関しては以下ドキュメントに纏められています。
より高度なワークフローを定義したい場合は下記サイトを見ながら実装しましょう。
https://docs.github.com/ja/actions/writing-workflows/workflow-syntax-for-github-actions#defaultsrun

ワークフローの名前

ワークフローには名前を付けられます。
今回は、『test_cicd』と付けます。

.yaml
name: test_cicd

トリガー条件

トリガー条件は 『on』で書きます。
例えば、mainブランチにpush/mergeされた時は以下になります。

.yml
name: test_cicd # ワークフローの名前
on: # トリガー条件
    push:
        branches:
            - main

branchesの箇所は複数ブランチを記載することもできます。

トリガー条件はcronコマンドによる定期実行などもできるらしいです。
バックアップなどに便利かもしれませんね。

on:
  schedule:
    - cron: '30 5 * * 1,3'

ジョブ

ジョブは「jobs」の中に複数に分けて記載します。
「jobs」の中にjob1,job2...のように分割したジョブを定義します。
これらのジョブはGithubから提供されるホストサーバー上で実行され、「runs-on」内にこのジョブを実行するOS環境を定義します。

それぞれのジョブは明記がない場合は並列実行されます。
直列実行させたい(前のジョブが成功した場合にこのジョブを実行)などは後述します。

一旦、ジョブ1から書いていきます。

.yml
name: test_cicd # ワークフローの名前
on: # トリガー条件
    push:
        branches:
            - main

jobs:
    job1:
        name: set-up-and-build-job
        runs-on: ubuntu-latest
        steps: 
            - name: Checkout code
              uses: actions/checkout@v2
        
            - name: Set up Node.js
              uses: actions/setup-node@v2
              with:
                node-version: '14'
        
            - name: Install dependencies
              run: npm install
        
            - name: Build project
              run: npm run build

            - name: upload build artifacts
              uses: actions/upload-artifact@v4
              with:
               name: build-artifacts
                path: ./build/*

jobsの中にstepsを記載してその中に実行するコマンドを記述していきます。
「run」は実際にコンソール上で実行するコマンド
「uses」はGitHub Marketplaceで提供されるアクションになります。

『uses: actions/setup-node@v2』はnode.jsをインストール
『uses: softprops/action-gh-release@v1』は指定した成果物フォルダをArtifactsとしてGithub上でダウンロードできるようになります。

actions/setup-node のドキュメントは以下になります。
https://github.com/actions/setup-node

upload-artifact のドキュメントは以下になります。
https://github.com/actions/upload-artifact?tab=readme-ov-file#usage

pushしてymlを反映

さて、ymlファイルを修正したらmainにpushしましょう。
pushすると早速、Github Actionsのワークフローが動作すると思います。

しかし、失敗してしまいました。。。
image.png

エラーが出たワークフローをクリックすると、どのステップでエラーが発生したのかが確認できます。
image.png

これを見るとnode.jsのインストール~npm installまでは成功しています。
npm run buildコマンド、つまりbuildで失敗しています。
さらに、コマンドの実行結果を確認できるのでエラー文を確認してみます。

image.png

Treating warnings as errors because process.env.CI = true.
Most CI servers set it automatically.

と書いてありますね。
これは 「process.env.CI = True」になっているとビルド実行中の「警告」も「エラー」として扱われます。
その下に「ESLint」とあり、僕のコードが指摘されています。
eslintはJavaScriptの静的解析ライブラリであり、npm run buildの際にセットで実行されています。この静的解析に引っかかってしまったためにビルドが失敗しています。

回避方法は2つあります。

  1. process.env.CI = False を指定
  2. 指摘された静的解析の箇所を修正

本来はビルドの前にdevelopへのマージタイミングで静的解析をするはずなので1.でもいいですが、今回は直すことにしました。

直してもう一回mainにpushします。
image.png

image.png

無事全てのステップが成功してワークフローのステータスも完了となりました。

Artifactsもしっかりと「1」となっており、ダウンロードもできます。
image.png

image.png

Artifactsはgithub側で管理されているストレージで保管されており、このストレージには容量の限界があります。
超過するとワークフローにエラーが発生したり、超過料金を取られます。
Artifactsの作成時には、数か月後に自動削除するなどのオプションも指定できるので実務で使用する際には必ず指定するようにしましょう。

ビルド成果物のサーバーへデプロイ方式の検討

さて、ワークフローでビルドを成功させることはできましたので続いてはこの成果物をサーバーへデプロイします。

デプロイには2つの方法があります。

1. SFTP(SCPコマンド)を用いてビルドフォルダを転送

一つ目はSFTPでファイルを転送する方法です。
こちらの方法はFWやセキュリティグループでSSH接続を許可しなければなりませんが、GithubActionsでホストされるサーバーに付与されるIPアドレスは動的に決定されます。
そのため、AWSのIAMユーザを使用するなどして一時的にFWやAWSのセキュリティグループにGithubActions用のIPアドレスを穴あけするなどの前処理が必要です。

そのため、
・穴あけ後にエラーが発生してセキュリティグループが開けっぱなしになる懸念(例外処理をするといいがコードへのある程度の理解が必須)
・ログ集約がだるい(セキュリティグループの穴あけなどはCloudTrailに飛ぶため)

などの問題があります。

2. APIGatewayを用意してCurlでHTTPリクエストを送ってLambdaを呼び出し、EC2にコマンドを流す

この方法だと
・セキュリティグループに穴あけが不要(というか既存リソースに影響を与えない)
・LambdaのログはCloudWatchLogsに流せるのでログ管理しやすい
というメリットがあります。

デメリット
・APIGatewayとLambdaを使用するのでコストが増加(大した額ではないが)
・APIGatewayとLambdaを使用することによる学習コスト
・セキュリティ上の懸念(HTTPS/IAM/エンドポイント認証の考慮が必須)
があります。
今回はLambdaの勉強にもなるので、2番で行きます。

デプロイ用のジョブを実装

まずはビルド成果物をpushする用のリポジトリを作成しましょう。
「react_build」という名称で作成しました。

新規リポジトリ作成後は必ず、空コミット(プッシュ)を行いましょう。
empty_repositoryだとブランチがない状態と判断されるのでエラーが発生します。

続いて、別リポジトリへのpushの際に認証トークンが必要となりますので作成します。

「setting」⇒「Tokens」⇒「Generate New Token」
image.png

image.png

image.png

トークンを作成するとトークンキーが生成されるので保存してください。
image.png

続いて、GithubActionsのSecretに先ほどのトークンキーを登録します。
プロジェクトの「setting」⇒「secret」⇒「Action」⇒「New repository secret」⇒「トークンキーのsecret追加」
image.png

image.png

image.png
↑今回はTOKEN_KEYという名前で登録しました。

このSecretを用いるとハードコーディングしたくない文字列を変数で扱えるので積極的に使用しましょう。

次に、yamlファイルにjob2を追加します。

main.yml
name: test_cicd # ワークフローの名前
on: # トリガー条件
    push:
        branches:
            - main

jobs:
    job1:
        name: set-up-and-build-job
        runs-on: ubuntu-latest
        steps: 
            - name: Checkout code
              uses: actions/checkout@v2
        
            - name: Set up Node.js
              uses: actions/setup-node@v2
              with:
                node-version: '14'
        
            - name: Install dependencies
              run: npm install
        
            - name: Build project
              run: npm run build
    
            - name: upload build artifacts #ビルド成果物アップロード
              uses: actions/upload-artifact@v4
              with:
                name: build-artifacts
                path: ./build/*
                
    job2: #新規作成
        name: push-to-reactbuild-repository
        runs-on: ubuntu-latest
        needs: job1 #job1が成功したら実行
        steps:
          - name: Checkout the target repository
            uses: actions/checkout@v4
            with:
              repository: 'masanori001/react_build'  # push先のリポジトリ
              ref: 'main'
              token: ${{ secrets.TOKEN_KEY }}  # GITHUB_TOKENを使って認証
    
          - name: Download build artifact
            uses: actions/download-artifact@v4
            with:
              name: 'build-artifacts' #ダウンロード対象のArtifacts
              path: ./build/*
    
          - name: Commit and push changes to another repository
            run: |
              git config user.name "<自分のユーザー名>"
              git add .
              git commit -m "Add build artifacts"
              git push origin main  # 別リポジトリの`main`ブランチにpush
            env:
              GITHUB_TOKEN: ${{ secrets.TOKEN_KEY }}  # GITHUB_TOKENを使って認証

以上のようになります。
このymlをpushしてワークフローの動作確認をします。

image.png

image.png

job2の方も成功しました。
react_buildリポジトリーを確認すると実際にpushされています。(コミットメッセージも一致)
image.png

気持ちいいですね。

次にこのreact_buildリポジトリへのpushをトリガーにこのリポジトリをpullさせていきます。

AWS設定

リソース作成

image.png
↑AWSのコンソールからAPIGatewayを作成します。
まだLambda関数を作成してないのでルート・ステージは適用で良いです。

image.png
↑作成出来たらコンソールに表示されます。

次にLambdaを作成します。
ランタイムは『node.js 22』で作成します。(言語はjavascriptとなります。)

image.png
↑『トリガー追加』でさっき作成した『APIGateway名』を指定します。

image.png
↑ APIGatewayが指し示すURLにアクセスするとLambda関数が実行されます。(リクエスト方式がANYなのでGETも受け付けてくれます)

次にLambda関数を経由してEC2にアクセスしてコマンドを実行するようにします。
Lambda関数からEC2にシェルスクリプトを流す際にはssh接続ではなく、SSMを使用できます。

SSMを使用する関係上
Lambda関数に『AmazonEC2ReadOnlyAccess』と『AmazonSSMFullAccess』ポリシーをアタッチします。
EC2はSSM接続ができる状態にしておく必要があります。
EC2のSSM接続手順は他記事で解説が多くあるため、割愛します。

次にLambda関数を変更します。

index.mjs
import { SSMClient, SendCommandCommand } from "@aws-sdk/client-ssm";

// SSMClient インスタンスを作成
const ssm = new SSMClient({});

export const handler = async (event) => {
    const params = {
        DocumentName: 'AWS-RunShellScript',
        Targets: [
            {
                Key: 'instanceIds',
                Values: [<デプロイ先となるEC2のインスタンスID>],
            },
        ],
        Parameters: {
            commands: [
                'echo "Hello, world!"',
                'cd /var/www/git/react_build/',  // gitリポジトリに移動
                'sudo git pull',  // git pull を実行
            ],
        },
    };

    try {
        // SendCommandCommand を作成して送信
        const command = new SendCommandCommand(params);
        const data = await ssm.send(command);
        console.log('Command sent successfully:', data);

        const commandId = data.Command.CommandId;

        // コマンドの実行結果を取得
        const commandInvocationParams = {
            CommandId: commandId,
            InstanceId: '<デプロイ先となるEC2のインスタンスID>',
        };

        // コマンド実行結果を取得
        const invocationData = await ssm.send(new SendCommandCommand(commandInvocationParams));
        console.log('Command execution result:', invocationData);

        return {
            statusCode: 200,
            body: JSON.stringify({
                message: 'Command sent and executed successfully',
                invocationData: invocationData,
            }),
        };
    } catch (err) {
        console.error('Error:', err);
        return {
            statusCode: 500,
            body: JSON.stringify({
                error: 'Failed to execute command',
                details: err,
            }),
        };
    }
};

インスタンスIDは別途、置き換えてください。

node.jsのランタイムバージョンによってAWS SDKのimport記述が変わるので注意してください。
node.js 18以降は「AWS SDK for javascript v3」のみ対応しているようです。
importの際に使用するコマンドもセットでimportする必要があったり、一括でimportなどはできないようです。
「@aws-sdk/client-ssm」のドキュメントを一応添付しておきます。
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-ssm/

デプロイ用リポジトリのYAMLファイル

APIGatewayのURLにCurlコマンドでPOSTすることでLambdaを発火させるだけです。
実際にはヘッダーに認証情報などを付加しますが今回はありません。

main.yaml
name: Notify on push

on:
  push:
    branches:
      - main

jobs:
  notify-webhook:
    runs-on: ubuntu-latest
    steps: #test
      - name: Send Webhook to External Server
        run: |
          curl -X POST <APIGatewayのURL> \
          -H "Content-Type: application/json" \
          -d '{"push": "true", "branch": "main", "commit": "${{ github.sha }}"}'

にはコンソールに表示されたURLに置き換えてください。
テスト用のpushをして着火します。

image.png
↑無事Curlが成功しています。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?