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?

GitHub Actions + SSM + AnsibleでSpring Bootアプリのデプロイを完全自動化してみた

0
Posted at

はじめに

前回の記事では、Terraformで構築したAWS環境に対してAnsibleを実行し、Spring Bootアプリをデプロイしました。

ただし、Ansibleの実行はローカルPCから手動で行っていたため、デプロイのたびに作業が発生していました。

そこで今回は、

  • GitHub Actions
  • AWS Systems Manager(SSM)
  • Ansible

を組み合わせて、

GitHubへPushするだけでEC2へデプロイできる仕組み

を構築しました。

また、SSH接続は使用せず、SSM経由でAnsibleを実行する構成にしています。


構成図

GitHub
    │
    ▼
GitHub Actions
    │
    ▼
SSM Run Command
    │
    ▼
EC2
    │
    ├─ Ansible実行
    ├─ Spring Bootビルド
    └─ systemd再起動

なぜSSHではなくSSMを選んだのか

一般的なデプロイ構成では、GitHub ActionsからSSH接続してEC2へログインするケースが多いと思います。

GitHub Actions
    │
    ▼
SSH
    │
    ▼
EC2

しかし、この方法には以下の課題があります。

  • SSH秘密鍵の管理が必要
  • GitHub Secretsへの秘密鍵登録が必要
  • Security Groupで22番ポートを開放する必要がある

そこで今回はSSM Run Commandを利用しました。

GitHub Actions
    │
    ▼
AWS API
    │
    ▼
SSM Agent
    │
    ▼
EC2

この構成であれば、

  • SSH鍵不要
  • 22番ポート不要
  • IAMによる権限制御が可能

となります。

GitHub Actions側のIAM設定

今回の構成では、GitHub Actionsから直接EC2へSSH接続するのではなく、
SSM Run Commandを利用してAnsibleを実行しています。

そのため、GitHub Actions側とEC2側の両方に適切なIAM権限が必要になります。
GitHub Actions用ロールには以下の権限を付与しています。

Run Commandの実行権限です。

statement {
  sid    = "AllowRunShellScriptDocument"
  effect = "Allow"

  actions = [
    "ssm:SendCommand"
  ]

  resources = [
    "arn:aws:ssm:${data.aws_region.current.id}::document/AWS-RunShellScript"
  ]
}

実行対象EC2はタグで制限しています。

statement {
  sid    = "AllowSendCommandToTaggedEc2"
  effect = "Allow"

  actions = [
    "ssm:SendCommand"
  ]

  resources = [
    "arn:aws:ec2:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:instance/*"
  ]

  condition {
    test     = "StringEquals"
    variable = "ssm:resourceTag/Project"
    values   = [var.project]
  }
}

また、実行結果確認用に以下も付与しています。

statement {
  sid    = "ReadSsmCommandResult"
  effect = "Allow"

  actions = [
    "ssm:GetCommandInvocation",
    "ssm:ListCommandInvocations",
    "ssm:ListCommands"
  ]

  resources = ["*"]
}

EC2側のIAMロール設定

SSM Run Commandを実行するためには、
GitHub Actions側だけでなく、
EC2側にもSystems Managerへ接続するための権限が必要です。

今回の構成では、
EC2にIAMロールをアタッチしています。

resource "aws_iam_role_policy_attachment" "ssm" {
  role       = aws_iam_role.ec2.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

AmazonSSMManagedInstanceCoreには、

  • SSM Agent登録
  • Run Command受信
  • Session Manager接続

に必要な権限が含まれています。

また、SSMの実行ログをCloudWatch Logsへ出力するため、
追加でCloudWatch Logsの権限を付与しました。

statement {
  sid = "CloudWatchLogs"

  actions = [
    "logs:CreateLogStream",
    "logs:DescribeLogStreams",
    "logs:PutLogEvents"
  ]

  resources = [
    "${aws_cloudwatch_log_group.ssm_run_command.arn}:*"
  ]
}

GitHub Actionsの処理フロー

今回のWorkflowでは以下の流れで処理を実行しています。

Terraform CD成功
    ↓
Terraform Output取得
    ↓
AnsibleのコードをZIP化
    ↓
S3へアップロード
    ↓
SSM Run Command実行
    ↓
EC2上でAnsible実行

TerraformのOutputを取得

Terraformで作成したEC2のインスタンスIDとRDSエンドポイントを取得しています。

- name: Get Terraform outputs
  working-directory: environments/dev
  run: |
    echo "EC2_INSTANCE_ID=$(terraform output -raw ec2_instance_id)" >> $GITHUB_ENV
    echo "RDS_ENDPOINT=$(terraform output -raw rds_endpoint)" >> $GITHUB_ENV

TerraformのOutputを利用することで、ハードコーディングを避けられます。


AnsibleコードをS3へアップロード

GitHub Actionsランナーから直接Ansibleを実行するのではなく、AnsibleコードをZIP化してS3へアップロードしています。

- name: Zip Ansible code
  run: |
    zip -r ansible.zip ansible

- name: Upload Ansible code to S3
  run: |
    aws s3 cp ansible.zip s3://${ANSIBLE_ARTIFACTS_BUCKET}/ansible/ansible.zip

後ほどEC2側でこのZIPファイルを取得します。


SSM Run Commandの実行

デプロイの中心となる部分です。

- name: Run Ansible on EC2 via SSM

内部では以下のコマンドを実行しています。

aws ssm send-command \
  --document-name "AWS-RunShellScript" \
  --instance-ids "${EC2_INSTANCE_ID}"

SSM Agentがコマンドを受け取り、EC2上で処理を実行します。


EC2側で実行している処理

EC2では以下の処理を順番に実行しています。

sudo dnf install -y unzip ansible-core

aws s3 cp s3://bucket-name/ansible/ansible.zip /tmp/ansible.zip

rm -rf /tmp/ansible

unzip -o /tmp/ansible.zip -d /tmp

cd /tmp/ansible

ansible-galaxy collection install community.mysql

ansible-playbook \
  -i inventory.ini \
  playbooks/site.yml \
  -e rds_endpoint="${RDS_ENDPOINT}"

処理内容は以下の通りです。

  1. Ansibleをインストール
  2. S3からPlaybook取得
  3. community.mysqlコレクション導入
  4. Playbook実行

SSM実行結果の監視

send-command実行後はCommand IDを取得し、実行結果をポーリングしています。

STATUS=$(aws ssm get-command-invocation \
  --command-id "${COMMAND_ID}" \
  --instance-id "${EC2_INSTANCE_ID}" \
  --query "Status" \
  --output text)

今回の実装では、

  • 10秒ごとに状態確認
  • 最大60回リトライ

としています。

その理由として、
Ansibleによるパッケージインストールや
アプリケーションのビルドに時間がかかる場合があり、
時間の猶予を持たせるためにこの設計にしました。

for i in {1..60}; do
  ...
  sleep 10
done

そのため、
10秒 × 60回 = 最大10分
まで待機する設定になっています。
デプロイが正常終了した場合は途中でループを抜けます。

一方で、

  • Failed
  • Cancelled
  • TimedOut

となった場合は即座に処理を終了します。

SSM実行失敗時にはWorkflowも失敗させています。

if [ "${STATUS}" != "Success" ]; then
  exit 1
fi

ハマったポイント

今回の構築では、

  • SSMコマンドが成功なのに何も起きない
  • CloudWatch Logsにログが出力されない
  • IAM権限不足によるエラー

など、いくつかのトラブルに遭遇しました。

内容が長くなってしまうため、詳細は別記事にまとめています。
公開後はこちらにリンクを追記します。

まとめ

今回はGitHub ActionsとSSMを利用し、AnsibleによるSpring Bootアプリのデプロイを自動化しました。

特にSSMを利用したことで、

  • SSH鍵不要
  • 22番ポート不要
  • IAMによるアクセス制御

を実現できました。

今後はEC2をPrivate Subnetへ移行し、よりセキュアな構成へ改善していきたいと思います。

次回予告

今回の構築では、

  • SSMコマンドが成功なのに何も起きない
  • CloudWatch Logsへログが出力されない
  • IAM権限不足によるTerraformエラー

など、さまざまなトラブルに遭遇しました。

次回は
「GitHub Actions + SSM + Ansibleで自動デプロイ環境を構築するときにハマったこと」

として、実際のエラーメッセージや調査手順をまとめたいと思います。

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?