はじめに
この記事は、以下のような課題をお持ちの方を対象としています。
- プライベートネットワーク上のRDSに安全にアクセスしたいが、従来のEC2踏み台サーバーの運用管理に手間を感じている
- 開発者や運用者に、SSHキーを配布することなく、必要な時だけコンテナへのシェルアクセスを許可したい
- 「誰が」「いつ」「どのデータベースを」操作したのか、証跡を確実に取得・管理したい
AWS環境において、セキュアなデータベースアクセスと信頼性の高い監査証跡の確保を両立させることは、運用上の重要な課題です。これまでAWS CLIからの利用が主だったECS Execですが、AWSマネジメントコンソールからも直接シェルアクセスが可能になり、その利便性は向上しました。(参考:ECS Exec が AWS マネジメントコンソールで利用可能に)
本記事では、このコンソールからのECS Execを中心に、SSH不要の踏み台を構築し、RDSの操作監査ログを取得するモダンな運用方法を、Terraformを使ったコード解説と共に紹介します。
以下が今回Terraformで構築を行う構成図となります。
【補足】データの暗号化について
この記事で解説する「セキュアな運用」とは、主にアクセス経路の制御(ECS Exec)と操作の証跡取得(監査ログ)を指しています。実際の環境では、これに加えてAWS Key Management Service (KMS)を利用し、保存されているデータそのものを暗号化する(データ保管時のセキュリティ)ことが強く推奨されます。今回はスコープ外としますが、KMSによる暗号化も併せて検討することで、より堅牢なセキュリティを実現できます。
【補足】シングルAZ構成の意図について
本記事のTerraformコードは、意図的にリソースを単一のAZに集約する構成にしています。
これは、今回構築するECSコンテナの役割が、常時稼働を求められるような本番的運用ではなく、開発者や運用者が必要な時にだけ利用する「踏み台(Bastion)」だからです。踏み台のような用途では、AZ障害によるわずかのなダウンタイムは許容できます。マルチAZ構成の複雑さや追加コストをかけるよりも、シンプルで安価なシングルAZ構成の方が合理的と言えます。
すべてのシステムで常にマルチAZが最適解とは限りません。AWSの設計では、リソースの「用途」「求められる可用性」のバランスを考え、最適な構成を選択することが重要です。
目次
今回利用するAWSサービスと技術の概要
- Amazon ECS Exec
ECS Execは、Amazon ECSの機能の一つで、稼働中のコンテナに対してインタラクティブなシェルアクセスや単発のコマンド実行を可能にします。AWSコンソール上のボタン一つでブラウザベースのターミナルを起動できるため、SSHクライアントもSSHキーも一切不要である点です。実体はAWS Systems Manager (SSM) の技術を利用しており、IAMの権限だけでセキュアなアクセス制御を実現します。(参考:AWS公式ドキュメント – ECS Exec を使用する) - 踏み台(Bastion)サーバー
セキュリティの基本原則として、データベースのような重要なリソースは、インターネットから直接アクセスできないプライベートなネットワークに配置します。利用者はまず踏み台サーバーにログインし、そこから目的のデータベースへアクセスします。これにより、アクセス経路を一元管理し、不正なアクセスを防ぎます。 - RDSの監査ログ (
pgaudit
)
「誰が、いつ、どのテーブルにどんなクエリを実行したのか」を記録することは、セキュリティ監査や障害発生時の原因調査において不可欠です。RDS for PostgreSQLでは、pgaudit
という拡張機能を利用することで、詳細な監査ログを取得できます。(参考:AWS公式ドキュメント – pgaudit を使用したデータベースアクティビティのログ記録)
【発展】AWS CLIを利用したポートフォワーディング
本記事では、ブラウザからコンソールを操作する手軽な方法を中心に解説しました。発展的な使い方として、ECS ExecはAWS CLIとSession Manager を組み合わせることで、ローカルPCとコンテナ間を接続する「ポートフォワーディング」という機能も利用できます。
この方法を使えば、データベースクライアントをローカルPCで実行し、あたかもRDSに直接接続しているかのように操作することが可能になります。
具体的な手順は本記事のスコープ外としますが、ECS Execにはこのような選択肢もあることを覚えておくと良いでしょう。
Terraformで実現するセキュアDB環境のポイント
それでは、実際にこの環境をコードで構築していきましょう。 この記事の付録で、今回使用するTerraformコードの全体を掲載します。
ここでは、コード全体の中から、今回の仕組みを実現する上で特に重要な4つのポイントに絞って解説します。
ポイント1: RDSの監査ログを有効化する設定
RDS for PostgreSQLで操作ログを取得するには、DBパラメータグループをカスタマイズしてpgaudit
を有効にする必要があります。database.tf
内の以下のコードがその役割を担っています。
database.tf
# Parameter Group to enable pgaudit
resource "aws_db_parameter_group" "postgres17_pgaudit" {
name = "ecs-exec-bastion-postgres17-pgaudit"
family = "postgres17"
parameter {
name = "shared_preload_libraries"
value = "pgaudit"
apply_method = "pending-reboot"
}
parameter {
name = "pgaudit.log"
value = "all"
}
}
# RDS Instance
resource "aws_db_instance" "main" {
# ... (一部抜粋) ...
parameter_group_name = aws_db_parameter_group.postgres17_pgaudit.name
# Send logs, including pgaudit logs, to CloudWatch Logs
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
}
shared_preload_libraries = "pgaudit"
: これがpgaudit
を有効化する最も重要な設定です。PostgreSQLの起動時にこの拡張機能を読み込むよう指示します。apply_method = "pending-reboot"
:shared_preload_libraries
は「静的」なパラメータであり、DBインスタンスの再起動を伴わないと適用できません。そのため、Terraformには即時適用ではなく「再起動待ち」状態で設定を投入するよう指示しています。この設定を有効化するには、後ほど手動での再起動が必要です。pgaudit.log = "all"
: どのような操作をログに記録するかを指定します。all
を指定すると、SELECT
やDML
(INSERT
,UPDATE
,DELETE
)、DDL
(CREATE TABLE
など) を含む、数多くの種類の操作が記録対象となります。enabled_cloudwatch_logs_exports
: 最後に、インスタンス側でpostgresql
ログのエクスポートを有効にすることで、pgaudit
が出力した監査ログがCloudWatch Logsに転送されるようになります。
ポイント2: ECS Execを有効化する設定
ECS Execは、ECSサービスとIAMロールの両方で有効化の設定が必要です。ecs.tf
内の以下のコードが対応します。
ecs.tf
# --- ECS Service ---
resource "aws_ecs_service" "bastion" {
# ... (一部抜粋) ...
# This is the key to enable ECS Exec
enable_execute_command = true
}
# --- IAM Policy for ECS Exec ---
resource "aws_iam_policy" "ecs_exec_policy" {
name = "ecs-exec-bastion-ecs-exec-policy"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
Resource = "*"
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_exec" {
role = aws_iam_role.ecs_task_role.name
policy_arn = aws_iam_policy.ecs_exec_policy.arn
}
enable_execute_command = true
: ECSサービスでこのフラグをtrue
に設定することが、ECS Execを有効にするには不可欠です。これがなければ、IAMで何を許可しようとECS Execは利用できません。- IAMポリシーのアクション: ECS Execは内部的にAWS Systems Manager (SSM) のメッセージング機能を利用してコンテナとの間にセキュアなトンネルを確立します。そのため、ECSタスクにアタッチするIAMロール(タスクロール)には、上記
ssmmessages:*
の4つのアクションを許可するポリシーが必要です。
ポイント3: セキュアな通信経路の確保
最後のポイントは、ネットワークアクセス制御です。「踏み台コンテナからRDSへのDB接続(TCP/5432)のみを許可する」という最小権限の原則を、セキュリティグループで実現します。
database.tf
# Security group for RDS
resource "aws_security_group" "rds" {
name = "ecs-exec-bastion-rds-sg"
vpc_id = aws_vpc.main.id
}
# Ingress from ECS Security Group
resource "aws_vpc_security_group_ingress_rule" "rds_from_ecs" {
security_group_id = aws_security_group.rds.id
referenced_security_group_id = aws_security_group.ecs.id
ip_protocol = "tcp"
from_port = 5432
to_port = 5432
}
ここで最も重要なのは、ingress
(インバウンド) ルールのsecurity_groups
に、ECSタスクが属するセキュリティグループのID (aws_security_group.ecs.id
) を指定している点です。
IPアドレス(CIDRブロック)で送信元を制限する代わりに、セキュリティグループIDで送信元を特定することで、「ecs-sg
に所属するリソースからの通信のみを許可する」という、より動的でセキュアなルールを定義できます。これにより、FargateタスクのIPアドレスが変わっても、ルールを修正する必要がなくなります。
ポイント4: 踏み台コンテナの準備と稼働維持
最後に、踏み台として機能するコンテナ自体を定義している部分です。このコンテナは、接続するためのDBクライアントを持ち、かつ私たちが接続するまで起動し続けてくれる必要があります。ecs.tf
内の以下のコードがその設定です。
ecs.tf
resource "aws_ecs_task_definition" "bastion" {
# ... (一部抜粋) ...
container_definitions = jsonencode([{
name = "bastion-container"
image = "public.ecr.aws/amazonlinux/amazonlinux:2023"
# Install psql client on startup and keep the container running
command = [
"sh",
"-c",
"dnf install -y postgresql17 && sleep 3600"
]
# ...
}])
}
ここで重要なのは、コンテナ起動時に実行されるcommand
です。
dnf install -y postgresql17
: 今回のコンテナOSであるAmazon Linux 2023では、dnf
コマンドを使ってPostgreSQL 17のクライアントツール(psql
)をインストールします。これがなければ、コンテナの中からRDSに接続することができません。&& sleep 3600
: そして、これが重要な工夫です。Fargateタスクは、フォアグラウンドで実行されるプロセスが終了すると、タスクも停止してしまいます。dnf
コマンドはインストールが完了すれば終了してしまうため、その後ろにsleep 3600
(1時間待機)というコマンドを繋げることで、コンテナがすぐに終了するのを防ぎ、私たちがECS Execで接続するための待機状態を維持しています。
この一行のコマンドが、踏み台コンテナに必要なツールを準備し、その役割を果たすための稼働状態を維持するという、2つの重要な役割を担っているのです。
【実践】ECS ExecでRDSに接続し、操作ログを確認する
Terraformによる環境構築が完了したところで、いよいよこの記事の核心部分である、実際の操作に移ります。ここからはAWSマネジメントコンソールを使い、以下の流れで作業を進めていきます。
- 手順1: DBインスタンスを再起動してログを有効化する
- 手順2: ECS Execを使って、コンソールから踏み台コンテナに接続する
- 手順3: 踏み台コンテナから、RDS for PostgreSQLに接続する
- 手順4: データベースを操作し、CloudWatch Logsで監査ログを確認する
⚠️ 重要なステップ
Terraformでpgaudit
を有効にする設定は投入されましたが、その設定を有効化するには、DBインスタンスを手動で1回再起動する必要があります。これを忘れると監査ログが出力されないため、必ず実施してください。
手順1: DBインスタンスを再起動して監査ログを有効化する
- AWSマネジメントコンソールでAmazon RDSのサービスページを開きます。
- 左側のメニューから「データベース」を選択し、Terraformが作成したDBインスタンス(
ecs-exec-bastion-db
)を選択します。 - 右上の「アクション」メニューから「再起動」をクリックします。
- 確認画面が表示されるので、そのまま「確認」ボタンを押して再起動を開始します。インスタンスのステータスが「再起動中」に変わり、数分後に「利用可能」に戻るのを待ちます。
手順2: AWSコンソールからECS Execでコンテナに接続する
SSHクライアントもSSHキーも使わずに、ブラウザ上のコンソールから直接コンテナのシェルを操作します。
- AWSマネジメントコンソールで Amazon ECS のサービスページを開きます。
- 左側のメニューから「クラスター」を選択し、Terraformで作成したクラスター(例:
ecs-exec-bastion-cluster
)をクリックします。
- 「タスク」タブを開き、現在実行中のタスクのIDをクリックします。
- タスクの詳細画面にある「接続」ボタンをクリックします。
- 「Execute Command」のダイアログが表示されます。「実行」をクリックします。
- 成功すると、画面にブラウザベースのターミナルが表示され、
bash-5.2#
のようなプロンプトが表示されます。これで、あなたはコンテナの中にいます!
手順3: 踏み台コンテナからRDSに接続する
次に、接続したコンテナの中から、プライベートサブネットにあるRDS for PostgreSQLデータベースに接続します。
- まず、データベースの接続情報を取得します。AWSマネジメントコンソールで AWS Secrets Manager のサービスページを開きます。
- Terraformが作成したシークレット(例:
ecs-exec-bastion/db-credentials-xxxxx
)を見つけてクリックします。 - 「シークレットの値」セクションにある「シークレットの値を取得」ボタンをクリックします。
- JSON形式でキーと値が表示されます。この中の
host
(RDSのエンドポイント)、username
、password
の値をメモしておきます。
- 先ほど開いたECS Execのターミナルに戻り、以下のコマンドを実行します。
|RDS_ENDPOINT|
と|DB_USERNAME|
の部分は、先ほどメモした値に置き換えてください。psql --host=|RDS_ENDPOINT| --port=5432 --username=|DB_USERNAME| --dbname=postgres
- パスワードの入力を求められるので、Secrets Managerからコピーしたパスワードを貼り付けてEnterキーを押します。
SSL connection (protocol: TLSv1.2, ...)
というメッセージに続き、プロンプトがpostgres=>
に変われば、RDSへの接続は成功です。このメッセージは、コンテナとDB間の通信が安全に暗号化されていることを示しています。
手順4: データベースを操作し、ログを確認する
最後に、データベースを実際に操作してログを生成し、CloudWatch Logsでその記録を確認します。
psql
のプロンプトで、ログに残すためのサンプルSQLをいくつか実行してみましょう。CREATE TABLE test_audit (id INT, message VARCHAR(255)); INSERT INTO test_audit (id, message) VALUES (1, 'ECS Exec audit test'); SELECT * FROM test_audit; DROP TABLE test_audit; -- 接続を終了します \q
- SQLの実行が終わったら、AWSマネジメントコンソールで CloudWatch のサービスページを開きます。
- 左側のメニューの「ログ」から「ロググループ」を選択します。
- フィルタに「
/aws/rds/instance/ecs-exec-bastion-db/postgresql
」と入力して、RDSのロググループを見つけてクリックします。 - ログストリームの一覧が表示されるので、最新のものをクリックします。
- ログイベントの中に、先ほど実行したSQLに対応する
pgaudit
のログが出力されていることが確認できます。実行したSQLクエリが記録されています。
このように、「誰が (IAMロール経由なので最終的にはコンテナ)」「いつ」「どんな操作をしたか」が、すべてログとして記録されていることが確認できました。
まとめ
本記事では、ECS Execを踏み台サーバーとして利用し、プライベートなRDSに安全に接続する方法、さらにRDSの操作監査ログを取得する仕組みをTerraformコードと共に解説しました。
EC2で踏み台サーバーを構築・運用する従来の方法と比較して、このアプローチには多くのメリットがあります。
- セキュリティの向上: SSHキーの管理が不要になり、IAMの権限のみでアクセス制御が可能です。また、踏み台コンテナ自体は外部からのインバウンド通信を一切許可しないため、攻撃対象領域を最小化できます。
- 運用負荷の軽減: サーバーレスのFargateを利用することで、踏み台サーバーのためのOSパッチ適用やミドルウェアの管理といった運用業務から解放されます。
- 監査証跡の強化: AWS CloudTrailには「誰がコンテナに接続したか(ECS Execの実行履歴)」が記録され、RDSの監査ログ(pgaudit)には「その人がデータベースで何をしたか(実行SQL)」が記録されます。この2つを組み合わせることで、強力な監査証跡となります。
ECS Execのコンソール上でのアクセスが可能になり、AWS上のリソースへのアクセス方法は、よりシンプルになりました。この記事が、皆さまのAWS環境のセキュリティと運用効率を向上させるための一助となれば幸いです。
付録:Terraformコード
以下に、この記事で解説したインフラを構築するためのTerraformコード全体を掲載します。コードの再現性を担保するため、実行環境のバージョンを以下に示します。
- Terraform:
1.13.3
- AWS Provider:
6.14.0
providers.tf
terraform {
required_version = "~> 1.13.3"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.14.0"
}
}
}
provider "aws" {
region = var.region
}
variables.tf
variable "region" {
description = "The AWS region to deploy resources."
type = string
default = "ap-northeast-1"
}
variable "project_name" {
description = "A prefix for all created resources."
type = string
default = "ecs-exec-bastion"
}
variable "vpc_cidr" {
description = "The CIDR block for the VPC."
type = string
default = "10.0.0.0/16"
}
network.tf
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.project_name}-vpc"
}
}
# Subnets
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.region}a"
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-subnet-public-a"
}
}
resource "aws_subnet" "private" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.region}a"
tags = {
Name = "${var.project_name}-subnet-private-a"
}
}
resource "aws_subnet" "db_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.11.0/24"
availability_zone = "${var.region}a"
tags = {
Name = "${var.project_name}-subnet-db-a"
}
}
resource "aws_subnet" "db_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.12.0/24"
availability_zone = "${var.region}c"
tags = {
Name = "${var.project_name}-subnet-db-c"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
# NAT Gateway
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public.id
depends_on = [aws_internet_gateway.main]
tags = {
Name = "${var.project_name}-ngw"
}
}
# Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = { Name = "${var.project_name}-rt-public" }
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = { Name = "${var.project_name}-rt-private" }
}
# Route Table Associations
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
subnet_id = aws_subnet.private.id
route_table_id = aws_route_table.private.id
}
resource "aws_route_table_association" "db_a" {
subnet_id = aws_subnet.db_a.id
route_table_id = aws_route_table.private.id
}
resource "aws_route_table_association" "db_b" {
subnet_id = aws_subnet.db_b.id
route_table_id = aws_route_table.private.id
}
database.tf
# --- RDS for PostgreSQL with Audit Logging ---
resource "aws_security_group" "rds" {
name = "${var.project_name}-rds-sg"
description = "Allow postgres access from ECS"
vpc_id = aws_vpc.main.id
}
resource "aws_vpc_security_group_ingress_rule" "rds_from_ecs" {
security_group_id = aws_security_group.rds.id
referenced_security_group_id = aws_security_group.ecs.id
ip_protocol = "tcp"
from_port = 5432
to_port = 5432
}
resource "aws_vpc_security_group_egress_rule" "rds_allow_all_outbound" {
security_group_id = aws_security_group.rds.id
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-rds-subnet-group"
subnet_ids = [aws_subnet.db_a.id, aws_subnet.db_b.id]
}
resource "aws_db_parameter_group" "postgres17_pgaudit" {
name = "${var.project_name}-postgres17-pgaudit"
family = "postgres17"
parameter {
name = "shared_preload_libraries"
value = "pgaudit"
apply_method = "pending-reboot"
}
parameter {
name = "pgaudit.log"
value = "all"
}
}
resource "aws_cloudwatch_log_group" "rds_audit" {
name = "/aws/rds/instance/${var.project_name}-db/postgresql"
retention_in_days = 7
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
engine = "postgres"
engine_version = "17.6"
instance_class = "db.t3.micro"
allocated_storage = 20
storage_type = "gp2"
username = "adminuser"
password = random_password.db.result
db_subnet_group_name = aws_db_subnet_group.main.name
parameter_group_name = aws_db_parameter_group.postgres17_pgaudit.name
vpc_security_group_ids = [aws_security_group.rds.id]
publicly_accessible = false
skip_final_snapshot = true
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
depends_on = [aws_cloudwatch_log_group.rds_audit]
}
# --- Secrets Manager for DB Credentials ---
resource "random_password" "db" {
length = 16
special = false
}
resource "random_pet" "secret_suffix" {
length = 2
}
resource "aws_secretsmanager_secret" "db_credentials" {
name = "${var.project_name}/db-credentials-${random_pet.secret_suffix.id}"
}
resource "aws_secretsmanager_secret_version" "db_credentials" {
secret_id = aws_secretsmanager_secret.db_credentials.id
secret_string = jsonencode({
username = aws_db_instance.main.username
password = random_password.db.result
host = aws_db_instance.main.address
port = aws_db_instance.main.port
dbname = aws_db_instance.main.db_name
})
}
ecs.tf
# --- ECS Cluster ---
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-cluster"
configuration {
execute_command_configuration {
logging = "DEFAULT"
}
}
}
# --- Security Group for ECS Task ---
resource "aws_security_group" "ecs" {
name = "${var.project_name}-ecs-sg"
description = "Security group for the ECS bastion task"
vpc_id = aws_vpc.main.id
}
resource "aws_vpc_security_group_egress_rule" "ecs_allow_all_outbound" {
security_group_id = aws_security_group.ecs.id
ip_protocol = "-1"
cidr_ipv4 = "0.0.0.0/0"
}
# --- IAM Roles and Policies for ECS Task ---
resource "aws_iam_role" "ecs_task_role" {
name = "${var.project_name}_ecs_task_role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
}
resource "aws_iam_policy" "ecs_exec_policy" {
name = "${var.project_name}_ecs_exec_policy"
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
Resource = "*"
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_exec" {
role = aws_iam_role.ecs_task_role.name
policy_arn = aws_iam_policy.ecs_exec_policy.arn
}
resource "aws_iam_role" "ecs_execution_role" {
name = "${var.project_name}_ecs_execution_role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = { Service = "ecs-tasks.amazonaws.com" }
}]
})
}
resource "aws_iam_role_policy_attachment" "ecs_execution" {
role = aws_iam_role.ecs_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# --- ECS Task Definition and Service ---
resource "aws_ecs_task_definition" "bastion" {
family = "${var.project_name}-bastion"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "256"
memory = "512"
task_role_arn = aws_iam_role.ecs_task_role.arn
execution_role_arn = aws_iam_role.ecs_execution_role.arn
container_definitions = jsonencode([{
name = "bastion-container"
image = "public.ecr.aws/amazonlinux/amazonlinux:2023"
command = [
"sh",
"-c",
"dnf install -y postgresql17 && sleep 3600"
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/${var.project_name}-bastion",
"awslogs-region" = var.region,
"awslogs-stream-prefix" = "ecs"
}
}
}])
}
resource "aws_cloudwatch_log_group" "ecs_task" {
name = "/ecs/${var.project_name}-bastion"
retention_in_days = 7
}
resource "aws_ecs_service" "bastion" {
name = "${var.project_name}-bastion-service"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.bastion.arn
launch_type = "FARGATE"
desired_count = 1
enable_execute_command = true
network_configuration {
subnets = [aws_subnet.private.id]
security_groups = [aws_security_group.ecs.id]
assign_public_ip = false
}
}