この記事は、ニフティグループ Advent Calendar 2023 7日目の記事です。
こんにちわ!NIFTY engineering運用チームのいかりがわです!
今回はWordPressのテーマをGitHub管理できるようにし、EC2に自動でデプロイするようにしたので、その手法をまとめていきたいと思います。
背景
私たちが運用しているNIFTY engineeringでは、何らかの変更があったときはFileManagerというWordPressのファイル管理用プラグインを使って手動で変更を加えていました。
しかし、これでは手動での変更反映になり、以下のような問題が起こります。
- 人為的なミスが発生する可能性が高まる
- 設定や手順のミス、環境の不整合などが起きやすくなります。
- 一貫性が保てない
- NIFTY engineeringでは、本番環境、開発環境の2つの環境を用意しています。 運用上では開発環境で確認後、本番環境にデプロイするようにしていますが、緊急時の対応などは急いで対応するため、本番のみに反映しがちになります。
ニフティのプロジェクトでは基本的にCI/CDパイプラインが整備されています。
NIFTY engineeringもCI/CDのパイプラインを使用して変更を容易にしていきたいと考えており、AWS CodePipelineによる自動デプロイのインフラを作っていこうと考えました。
手法
私たちは、GitHub ActionsからAWS CodeCommitへソースコードをコピーして、CodePipelineを使用するようにしました。
また、WordPressのテーマをEC2に自動デプロイする手法を採用しました。
具体的な流れは以下の通りです。
- まず、DeveloperがソースコードをGitHubリポジトリにプッシュします。
- GitHub Actionsはリポジトリの変更を監視し、変更があったらトリガーします。
- 変更があると、GitHub ActionsはCodeCommitにソースコードをコピーします。
- CodeCommit内のコードが変更されると、CodePipelineがトリガーします。
- CodePipelineのデプロイフェーズにてAWS CodeDeployが実行されます。
- CodeDeployにて、EC2の特定のディレクトリに変更をデプロイします。
これで、GitHubでのソースコードの管理が容易になり、自動デプロイによる効率的な運用が可能となります。
構成図
構成図は以下のようになっています。構成図内の処理の順番は手法の手順に沿っています。
TerraformでAWSのリソースを作る
では実際にCI/CDインフラを作ってみたいと思います。
まず、AWSのリソースをTerraformで作っていきます。
ちなみにここではTerraformのプロバイダ設定やバージョンの定義は割愛して、リソースの定義のみにしています。
また、EC2インスタンスはすでに起動していることを想定します。
CodeCommit
CodeCommitのリポジトリを作成します。リポジトリ名を指定するだけです。
1 2 3 4 |
resource "aws_codecommit_repository" "repository" { repository_name = "codecommit-repository" } |
CodeDeploy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
resource "aws_iam_role" "deploy" { assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Sid = "" Principal = { Service = "codedeploy.amazonaws.com" } }, ] }) } resource "aws_iam_role_policy_attachment" "deploy" { role = aws_iam_role.deploy.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole" } resource "aws_codedeploy_app" "deploy" { compute_platform = "Server" name = "deploy" } resource "aws_codedeploy_deployment_group" "deploy" { app_name = aws_codedeploy_app.deploy.name deployment_group_name = aws_codedeploy_app.deploy.name deployment_config_name = "CodeDeployDefault.OneAtATime" deployment_style { deployment_option = "WITHOUT_TRAFFIC_CONTROL" deployment_type = "IN_PLACE" } ec2_tag_set { ec2_tag_filter { key = "Name" type = "KEY_AND_VALUE" value = {デプロイ先EC2インスタンス名} } } service_role_arn = aws_iam_role.deploy.arn auto_rollback_configuration { enabled = true events = ["DEPLOYMENT_FAILURE"] } } |
aws_iam_role、aws_iam_role_policy_attachmentを使用し、CodeDeployで使用するIAMロールを作成します。
指定したポリシーはマネージドポリシーのAWSCodeDeployRoleです。
また、aws_codedeploy_appを使用して、CodeDeployのリソースを作成し、aws_codedeploy_deployment_groupにて、CodeDeployで使用するデプロイメントグループを作成します。
以下のようにec2_tag_setでNameタグを指定することで、その名前と一致するEC2インスタンスを指定することができます。 これにより、CodeDeployがEC2へデプロイできるようになります。
1 2 3 4 5 6 7 8 |
ec2_tag_set { ec2_tag_filter { key = "Name" type = "KEY_AND_VALUE" value = {デプロイ先EC2インスタンス名} } } |
CodePipeline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
resource "aws_iam_role" "pipeline" { assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Sid = "" Principal = { Service = "codepipeline.amazonaws.com" } }, ] }) } resource "aws_iam_policy" "pipeline" { name = "codepipeline-policy" path = "/service-role/" description = "Policy for CodePipeline to deploy" policy = file("policies/codepipeline_policy.json") } resource "aws_iam_role_policy_attachment" "pipeline" { role = aws_iam_role.pipeline.name policy_arn = aws_iam_policy.pipeline.arn } resource "aws_codepipeline" "pipeline" { name = "pipeline" role_arn = aws_iam_role.pipeline.arn artifact_store { location = "unique-s3-bucket-name" type = "S3" } stage { name = "Source" action { category = "Source" configuration = { BranchName = "main" PollForSourceChanges = false RepositoryName = "codecommit-repository" } name = "Source" output_artifacts = ["SourceArtifact"] owner = "AWS" provider = "CodeCommit" run_order = "1" version = "1" } } stage { action { category = "Deploy" configuration = { ApplicationName = aws_codedeploy_app.deploy.name DeploymentGroupName = aws_codedeploy_app.deploy.name } input_artifacts = ["SourceArtifact"] name = "Deploy" owner = "AWS" provider = "CodeDeploy" run_order = "1" version = "1" } name = "Deploy" } depends_on = [aws_codedeploy_deployment_group.deploy] } |
aws_iam_role、aws_iam_policy、aws_iam_role_policy_attachmentを使用して、CodePipeline用IAMロールを作成しています。
また、aws_iam_policyではpolicies/codepipeline_policy.jsonというファイルを参照しており、そこにポリシーの設定が記述されています。(後述)
aws_codepipelineを使用し、CodePipelineのリソースを定義していきます。
stageを使用することで、CodePipeline内の各フェーズを定義することができます。
今回は上で定義したCodeCommitとCodeDeployのリソースを紐付け、フェーズとして定義していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
stage { name = "Source" action { category = "Source" configuration = { BranchName = "main" PollForSourceChanges = false RepositoryName = "codecommit-repository" } name = "Source" output_artifacts = ["SourceArtifact"] owner = "AWS" provider = "CodeCommit" run_order = "1" version = "1" } } stage { action { category = "Deploy" configuration = { ApplicationName = aws_codedeploy_app.deploy.name DeploymentGroupName = aws_codedeploy_app.deploy.name } input_artifacts = ["SourceArtifact"] name = "Deploy" owner = "AWS" provider = "CodeDeploy" run_order = "1" version = "1" } name = "Deploy" } |
CodePipeline用IAMロール
AWS公式のユーザーガイドを参考に作成しています。
https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/security-iam.html
policies/codepipeline_policy.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
{ "Statement": [ { "Action": [ "iam:PassRole" ], "Resource": "*", "Effect": "Allow", "Condition": { "StringEqualsIfExists": { "iam:PassedToService": [ "ec2.amazonaws.com" ] } } }, { "Action": [ "codecommit:CancelUploadArchive", "codecommit:GetBranch", "codecommit:GetCommit", "codecommit:GetUploadArchiveStatus", "codecommit:UploadArchive" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "codedeploy:CreateDeployment", "codedeploy:GetApplication", "codedeploy:GetApplicationRevision", "codedeploy:GetDeployment", "codedeploy:GetDeploymentConfig", "codedeploy:RegisterApplicationRevision" ], "Resource": "*", "Effect": "Allow" }, { "Action": [ "ec2:*", "cloudwatch:*", "s3:*", "sns:*" ], "Resource": "*", "Effect": "Allow" } ], "Version": "2012-10-17" } |
CodePipeline用S3バケット
CodePipelineでは各フェーズ間のデータの受け渡しに使用されます。
今回の例だと、CodeCommitからCodeDeployへソースコードを受け渡すために使用されます。
1 2 3 4 5 6 7 8 9 10 11 12 |
resource "aws_s3_bucket" "codepipeline" { bucket = "unique-s3-bucket-name" } resource "aws_s3_bucket_ownership_controls" "codepipeline" { bucket = aws_s3_bucket.codepipeline.id rule { object_ownership = "BucketOwnerEnforced" } } |
EventBridge
これまでの定義で一通りのリソースは定義されました。しかし、これでは何をもってCodePipelineが実行され、デプロイが走るのかが定義されていません。
ここでEventBridgeを定義し、CodeCommitのリポジトリでソースコードが変更されたときにCodePipelineがトリガーされるようにしていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
resource "aws_iam_policy" "events_trigger" { description = "Policy for notice update to codepipeline" name = "events-trigger-policy" path = "/service-role/" policy = jsonencode({ "Statement": [ { "Action": [ "codepipeline:StartPipelineExecution" ], "Effect": "Allow", "Resource": "*", } ], "Version": "2012-10-17" }) } resource "aws_iam_role" "events_trigger" { name = "events-trigger-role" assume_role_policy = jsonencode({ "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com" } } ], "Version": "2012-10-17" }) } resource "aws_iam_role_policy_attachment" "events_trigger" { role = aws_iam_role.events_trigger.name policy_arn = aws_iam_policy.events_trigger.arn } resource "aws_cloudwatch_event_rule" "trigger" { name = "codecommit-trigger-rule" description = "update event" event_pattern = templatefile("events/events_trigger_rule.tpl.json", { repository_arn = aws_codecommit_repository.repository.arn }) is_enabled = "true" } resource "aws_cloudwatch_event_target" "trigger" { arn = aws_codepipeline.pipeline.arn role_arn = aws_iam_role.events_trigger.arn rule = aws_cloudwatch_event_rule.trigger.name } |
IAMロールはCodePipelineと同様のやり方で定義しています。
EventBridgeはCodePipelineを実行することができれば良いので、StartPipelineExecutionというポリシーのみ定義しています。
1 2 3 4 5 6 7 8 9 10 11 12 |
{ "Statement": [ { "Action": [ "codepipeline:StartPipelineExecution" ], "Effect": "Allow", "Resource": "*" } ], "Version": "2012-10-17" } |
イベントパターン
イベントパターンの定義は別ファイルで定義しています。
CodeCommitリポジトリでソースコードが変更されたときにトリガーされるようにしています。
events/events_trigger_rule.tpl.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
{ "detail": { "event": [ "referenceCreated", "referenceUpdated" ], "referenceName": [ "${branch_name}" ], "referenceType": [ "branch" ] }, "detail-type": [ "CodeCommit Repository State Change" ], "resources": [ "main" ], "source": [ "aws.codecommit" ] } |
IAM
最後にIAMユーザーです。
GitHubからCodeCommitへソースコードをプッシュできるようにするために作成します。
作成したIAMユーザーはGitHubリポジトリのシークレットに紐づけて使用します。
1 2 3 4 5 6 7 8 9 10 11 |
resource "aws_iam_user" "github" { force_destroy = "false" name = "github-user" path = "/" } resource "aws_iam_user_policy_attachment" "github_codecommit" { user = aws_iam_user.github.name policy_arn = "arn:aws:iam::aws:policy/AWSCodeCommitPowerUser" } |
Terraformでの反映
applyして、AWSの環境にリソースを追加します。
1 |
terraform apply |
無事反映されました!例としてCodePipelineの画面です。
SSHキーの登録
ローカル環境にて、ssh-keygenコマンドを実行します。
1 |
ssh-keygen |
保存先とパスフレーズの入力を求められるので任意の保存先、パスフレーズを入力します。
1 2 3 |
Generating public/private rsa key pair. Enter file in which to save the key (~~~/.ssh/id_rsa): Enter passphrase (empty for no passphrase): |
指定した保存先にid_rsaとid_rsa.pubが出力されました。
id_rsaをGitHubへ、id_rsa.pubをIAMユーザーへそれぞれ登録していきます。
1 2 3 4 |
ls -la | grep id_rsa -rw------- 1 username staff 2610 12 6 13:43 id_rsa -rw-r--r-- 1 username staff 579 12 6 13:43 id_rsa.pub |
IAMユーザーにSSH公開キーを登録
id_rsa.pubを開いて、中身をIAMユーザーに登録します。
コンソールからIAMユーザーのページへ遷移し、Terraformで作成したIAMユーザーを探します。
IAMユーザーを選択し、セキュリティ認証情報へ遷移します。
すると、AWS CodeCommitのSSH公開キーという項目があるので、SSH公開キーのアップロードを選択します。
そして、先ほど作成したid_rsa.pubの内容をコピペします。
すると、SSHキーIDが表示されるようになるので、これをコピーしておきます。(GitHubに登録します)
IAMユーザーの設定は完了です。
GitHubに非公開キーとIAMユーザーのIDを登録
続いてGitHub Actionsに必要なシークレットを登録していきます。
必要なシークレットは以下のようになっています。
- CODECOMMIT_SSH_USERNAME
- IAMユーザーのSSHキーID
- CODECOMMIT_SSH_PRIVATE_KEY
- id_pubの中身
GitHubリポジトリのページより、Settingsへ遷移します。
そして、左メニューからSecrets and variablesより、Actionsの項目を選択します。
ここからRepository secretsを登録できるので、New repository secretsで変数を登録していきます。
これでSSHキーの登録は完了です。
GitHub Actions
続いてGitHub Actionsを作っていきます。
簡単にディレクトリ構成を紹介します。
対象のリポジトリは以下のようなディレクトリ構成をしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
. ├── .github │ └── workflows │ ├── mirror_codecommit.yaml │ └── scripts │ └── mirror.sh ├── .gitignore ├── README.md ├── appspec.yml └── myTheme ├── <テーマに関連するファイル群> ├── : └── : |
- .github/workflows/mirror_codecommit.yaml
- 実行されるGitHub Actionsの設定ファイル
- .github/workflows/scripts/mirror.sh
- 上記GitHub Actionsにて、実行されるシェルスクリプト
- このシェルスクリプトでCodeCommitへのコピーを行います
- appspec.yml
- CodeDeployで使用されるデプロイの手順を定義する設定ファイル
- myTheme/
- WordPressに配置されるディレクトリ
- デプロイ対象となる
mirror_codecommit.yaml
GitHub Actionsの設定ファイルを作成していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
name: Mirror codes to CodeCommit on: push: branches: - "main" permissions: contents: read jobs: mirror-to-codecommit: name: Mirror codes to CodeCommit runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Git push env: TARGET_REPO_URL: ssh://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/<CodeCommitのリポジトリ名> SSH_USERNAME: ${{ secrets.CODECOMMIT_SSH_USERNAME }} SSH_PRIVATE_KEY: ${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY }} run: bash "${GITHUB_WORKSPACE}/.github/workflows/scripts/mirror.sh" |
ここではmainブランチへのプッシュを検知し、ソースコードをCodeCommitリポジトリにミラーリングするジョブを定義しています。
後述のシェルスクリプトを使用してCodeCommitにプッシュが行われます。
SSHキーは先ほど登録したシークレットから取得されます。
mirror.sh
続いて、GitHub Actionsから実行されるシェルスクリプトです。
1 2 3 4 5 6 7 8 9 10 11 12 |
#!/usr/bin/env sh set -eu # copy credential mkdir -p ~/.ssh echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa # push to mirror rpository export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l $SSH_USERNAME" git remote add mirror "$TARGET_REPO_URL" git push --tags --force --prune mirror "refs/remotes/origin/*:refs/heads/*" |
あらかじめGitHub Actions上で登録された環境変数を読み取り、SSHキーなどのプッシュに必要な情報を取得します。
その情報を使用して、指定のCodeCommitのリポジトリにコードをプッシュしています。
appspec.yml
最後にappspec.ymlです。こちらはCodeDeployで使用されるデプロイの手順を定義する設定ファイルとなっています。
1 2 3 4 5 6 |
version: 0.0 os: linux files: - source: myTheme/ destination: /bitnami/wordpress/wp-content/themes/myTheme file_exists_behavior: OVERWRITE |
filesを使用して、デプロイ元のファイルと、デプロイ先EC2インスタンスのディレクトリを指定します。
今回の例だと、myTheme配下にテーマを配置しているので、こちらをデプロイするようにしています。
1 2 3 |
files: - source: myTheme/ destination: /bitnami/wordpress/wp-content/themes/myTheme |
また、以下の設定を追加することで、ファイルが存在する場合は上書きするように設定しました。
1 |
file_exists_behavior: OVERWRITE |
これで、全ての設定が完了しました!
動かしてみる
全ての設定が完了したので、実際にリポジトリにプッシュして、GitHub ActionsやCodePipelineが動作することを確認してみます。
変更を反映していつものようにリポジトリにプッシュしてみます。
1 |
git push origin main |
すると、GitHub Actionsが動き出しました!
CodeCommitにミラーリングされています! (リポジトリ名などが違いますがそこはご愛嬌…)
CodePipelineもこんな感じで実行されています!
これで一通りのデプロイまでの流れを試すことができました。
まとめ
今回はCodePipelineを使ってWordPressのテーマをデプロイしてみました。
これでNIFTY engineeringの変更が容易になり、より快適なブログライフを送ることができるようになりました!
WordPressのGitHub管理やCI/CDは悩ましいところが多いのでぜひ参考にしてみてください!
明日は、@kanishionoriさんの「今すぐ1on1をやった方がいい3つの理由」です。お楽しみに!