はじめに
前回の記事では、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}"
処理内容は以下の通りです。
- Ansibleをインストール
- S3からPlaybook取得
- community.mysqlコレクション導入
- 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で自動デプロイ環境を構築するときにハマったこと」
として、実際のエラーメッセージや調査手順をまとめたいと思います。