はじめに
アプリケーション開発において、セキュリティとデプロイの効率性を両立することは常に課題です。特にSSH22ポートを公開してのデプロイは、ブルートフォース攻撃などのセキュリティリスクが伴います。
また、今回の記事内容はサンプルであり、
私自身がインフラやセキュリティに詳しい訳ではないため
設定やコードを使用する場合は自己責任でお願いします。
本記事では、従来のSSH接続によるデプロイから AWS Session Manager + S3 を活用したセキュアなデプロイ環境への移行について、実際の実装手順から遭遇した問題まで詳しく解説します。
なお、今回のAWS Session Manager + S3でのデプロイ実装は、
自身が作っていた既存のコードをベースにしながら、一定の大きさ以上のファイルをSSH接続を使わずに行いたかったという前提とCodeDeployに変えるには少し大掛かりになりそうだったという前提があります。
そのため、一般的なデプロイ設計としてはAWS CodeDeployなどをお勧めします。
あくまで こういったやり方もできました。 という感じで読んでください。
💡 この記事で得られること
- SSH22ポート廃止によるセキュリティ向上手法
- AWS Session Manager + S3を使ったCI/CDパイプライン設計
- GitHub ActionsとAWSサービスの連携実装
- 実際に遭遇した問題とその解決方法
- セキュリティとコストのバランス取り
🎯 背景と課題
従来の構成とリスク
開発スピードを重視した初期段階のプロジェクトや、レガシーシステムでは、SSH接続によるデプロイフローが採用されていることがあります。以下はその典型的な構成例です。
# 従来のSSH方式
- name: Deploy with Rsync
uses: burnett01/rsync-deployments@5.1
with:
switches: '-avz --delete'
path: build/libs/*.jar
remote_path: /srv/app/
remote_host: ${{ secrets.HOST }}
remote_user: ${{ secrets.USER }}
remote_key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Restart application
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
sudo systemctl restart app-service
主な問題点
- SSH22ポート公開:ブルートフォース攻撃の標的になりやすい
- アクセス制御:IP制限だけでは完全なセキュリティ確保が困難
- 監査ログ:SSH接続の詳細な操作履歴が取りづらい
🔍 解決方法の検討
検討した選択肢
方式 | メリット | デメリット | 判定 |
---|---|---|---|
VPNソリューション | 高セキュリティ | インフラ管理複雑、コスト高 | ❌ |
AWS CodeDeploy | AWS標準、高機能 | 既存変更大、学習コスト高 | ❌ |
Session Manager直接転送 | シンプル | ファイルサイズ制限 | ❌ |
Session Manager + S3 | セキュア、柔軟性高 | 実装工数やや増 | ✅ |
最終選択:Session Manager + S3
選択理由:
- SSH22ポートが完全に不要
- AWS標準サービスのみで構成
- 既存のSSHデプロイロジックを大きく変更せずに済む
- セキュリティレベルの大幅向上
- 監査ログ対応
🏗️ アーキテクチャ設計
システム構成
デプロイフロー
🛠️ 実装手順
Step 1: IAM設定
重要:
- S3バケット名は全世界で一意である必要があるため、以下の例の
deploy-bucket-*
は実際にはdeploy-<組織名>-<プロジェクト名>-*
のような一意な名前に変更してください。 - Session Manager関連のアクション(ssm、ssmmessages、ec2messages)は、その性質上
Resource: "*"
を指定する必要があるようです。
EC2用IAMロール(例
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:UpdateInstanceInformation",
"ssm:SendCommand",
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel",
"ec2messages:*"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket",
"s3:CreateBucket"
],
"Resource": [
"arn:aws:s3:::deploy-bucket-*",
"arn:aws:s3:::deploy-bucket-*/*"
]
}
]
}
GitHub Actions用IAMユーザー設定(例
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand",
"ssm:GetCommandInvocation",
"ssm:DescribeInstanceInformation"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:CreateBucket",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::deploy-bucket-*",
"arn:aws:s3:::deploy-bucket-*/*"
]
}
]
}
Step 2: GitHub Actions Secrets
AWS_ACCESS_KEY_ID: <IAMユーザーのアクセスキー>
AWS_SECRET_ACCESS_KEY: <IAMユーザーのシークレットキー>
AWS_REGION: <リージョン名>
EC2_INSTANCE_ID: i-xxxxxxxxxxxxxxxxx
EC2_USER: <EC2ユーザー名>
Step 3: GitHub Actionsワークフロー
Java/Spring Boot アプリケーション例
name: Deploy Application
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
# ビルド処理
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Java
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Build with Gradle
run: ./gradlew clean build
# AWS認証設定
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
# S3アップロード
- name: Upload to S3
run: |
JAR_FILE=$(ls build/libs/*.jar | head -1)
JAR_NAME=$(basename "$JAR_FILE")
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
# プロジェクト固有の名前を使用(例:組織名-プロジェクト名-リージョン)
S3_BUCKET="deploy-myorg-myapp-${{ secrets.AWS_REGION }}"
S3_KEY="deploys/${TIMESTAMP}/${JAR_NAME}"
# S3バケット作成(存在しない場合)
aws s3 mb "s3://${S3_BUCKET}" --region ${{ secrets.AWS_REGION }} 2>/dev/null || true
# JARファイルをアップロード
aws s3 cp "$JAR_FILE" "s3://${S3_BUCKET}/${S3_KEY}" --region ${{ secrets.AWS_REGION }}
echo "S3_BUCKET=${S3_BUCKET}" >> $GITHUB_ENV
echo "S3_KEY=${S3_KEY}" >> $GITHUB_ENV
# Session Manager経由でデプロイ
- name: Deploy via Session Manager
run: |
COMMAND_ID=$(aws ssm send-command \
--instance-ids ${{ secrets.EC2_INSTANCE_ID }} \
--document-name "AWS-RunShellScript" \
--parameters "commands=[
\"sudo systemctl stop app-service\",
\"aws s3 cp s3://${{ env.S3_BUCKET }}/${{ env.S3_KEY }} /tmp/app.jar\",
\"sudo mv /tmp/app.jar /srv/app/\",
\"sudo chmod 755 /srv/app/app.jar\",
\"sudo chown ${{ secrets.EC2_USER }}:${{ secrets.EC2_USER }} /srv/app/app.jar\"
]" \
--region ${{ secrets.AWS_REGION }} \
--output text --query "Command.CommandId")
# コマンド実行完了を待機
aws ssm wait command-executed \
--command-id $COMMAND_ID \
--instance-id ${{ secrets.EC2_INSTANCE_ID }} \
--region ${{ secrets.AWS_REGION }}
# 実行結果確認
STATUS=$(aws ssm get-command-invocation \
--command-id $COMMAND_ID \
--instance-id ${{ secrets.EC2_INSTANCE_ID }} \
--region ${{ secrets.AWS_REGION }} \
--output text --query "Status")
if [ "$STATUS" != "Success" ]; then
echo "Deployment failed with status: $STATUS"
exit 1
fi
# アプリケーション再起動
- name: Restart application
run: |
aws ssm send-command \
--instance-ids ${{ secrets.EC2_INSTANCE_ID }} \
--document-name "AWS-RunShellScript" \
--parameters 'commands=["sudo systemctl start app-service"]' \
--region ${{ secrets.AWS_REGION }}
# S3クリーンアップ
- name: Clean up old files
run: |
aws s3 rm "s3://${{ env.S3_BUCKET }}/deploys/" \
--recursive \
--exclude "*" \
--include "deploys/*" \
--exclude "deploys/$(date -d '7 days ago' +%Y%m%d)*" || true
Step 4: EC2環境準備
# AWS CLI v2インストール(Ubuntu/Debian系の例)
sudo apt update
sudo apt install -y unzip curl
# AWS CLI v2のダウンロードとインストール
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# インストール確認
aws --version
# 出力例: aws-cli/2.x.x Python/3.x.x Linux/x.x.x
注意: Amazon Linux 2やRHEL系の場合は、パッケージマネージャーが異なるため適宜調整してください。
🚨 実装時の問題と解決
問題1: AWS CLI not found
症状:
StandardErrorContent: "aws: not found"
原因: EC2にAWS CLIがインストールされていない
解決: EC2に事前にAWS CLI v2をインストール
問題2: s3:GetObject権限不足
症状: ファイルが更新されない
原因: EC2のIAMロールにs3:GetObject
権限がなかった
解決: IAMロールに適切なS3権限を追加
問題3: ファイルサイズ制限(初期検討時)
症状:
Argument list too long
原因: Session Manager経由でのBase64転送でコマンドライン制限
解決: S3を中間ストレージとして活用
📊 移行前後の比較
アーキテクチャ比較
詳細比較表
項目 | SSH方式 | Session Manager + S3方式 |
---|---|---|
セキュリティ | SSH22ポート公開必要 | ポート公開不要 |
認証方式 | SSH鍵 | IAM |
監査ログ | 限定的 | CloudTrail対応 |
攻撃面 | SSH22ポート | なし |
運用負荷 | SSH鍵管理必要 | 不要 |
コスト | 0円 | 数円〜数十円/月(S3使用料) |
実行時間 | ~1-2分 | ~1-2分(変化なし) |
💰 コスト分析
事前のコスト試算
移行前に S3 使用料金を試算した結果、以下のような想定となりました。
- バックエンドアプリ: ~50-100MB × 30回/月 = ~1.5-3GB
- フロントエンドアプリ: ~10-20MB × 30回/月 = ~300-600MB
- 合計転送量: ~2-4GB/月
- 想定コスト: 数円〜数十円/月(東京リージョンの場合)
この試算結果から、セキュリティ向上のメリットに対してコスト増加は許容範囲内と判断し、実装に踏み切りました。
結論: 事前試算通り、セキュリティを大幅に向上させながらコスト増加は最小限に抑えられることを確認
🎯 得られた成果
セキュリティ向上
- ✅ SSH22ポート完全廃止
- ✅ IAMベース認証システム
- ✅ AWS標準の暗号化通信
- ✅ 監査ログ対応(CloudTrail)
運用効率化
- ✅ SSH鍵管理からの解放
- ✅ IP制限設定不要
- ✅ 自動化されたデプロイフロー維持
- ✅ 緊急時の復旧体制
開発体験
- ✅ デプロイ手順の変更なし(開発者視点)
- ✅ 既存ワークフローとの互換性
- ✅ トラブルシューティングの容易さ
🔧 トラブルシューティング
デバッグ方法
# Session Manager実行結果の詳細確認
aws ssm get-command-invocation \
--command-id <COMMAND_ID> \
--instance-id <INSTANCE_ID> \
--region <REGION> \
--output json
# S3ファイル確認
aws s3 ls s3://deploy-bucket-<region>/deploys/ --recursive
# EC2ファイル更新確認
ls -la /srv/app/app.jar
よくある問題
-
Status: Failed →
StandardErrorContent
を確認 - aws: not found → EC2にAWS CLIをインストール
- Permission denied → IAM権限を確認
-
File not updated →
s3:GetObject
権限を確認
📚 学んだベストプラクティス
- 段階的移行:一気に変更せず、バックアップを保持しながら段階的に
- AWS標準サービス活用:独自ソリューションより信頼性の高い標準サービス
- 詳細ログ:トラブルシューティングのために十分なログ出力
- 権限最小化:必要最小限のIAM権限設定
- コスト意識:セキュリティ向上とコストのバランス
まとめ
SSH22ポートの廃止は、一見複雑に思えるかもしれませんが、AWS Session Manager + S3を活用することで、セキュリティを大幅に向上させながらも運用負荷を軽減できました。
主な成果:
- セキュリティリスクの大幅削減
- 運用負荷の軽減(SSH鍵管理不要)
- コスト増加の最小化(月5円程度)
- 開発体験の維持
特に、一定以上のセキュリティ要件を満たしながら、開発者の日常業務に影響を与えない点は重要なポイントです。
同様の課題を抱えている方の参考になれば幸いです。実装時の詳細な技術的質問があれば、コメントでお気軽にお聞きください!
参考リンク
この記事は実際のプロダクション環境での移行経験をもとに書かれています。環境や要件に応じて適宜調整してご利用ください。