From 7ab6c3fc164c2cb1f7136df1bb0ff143c2cf9cf2 Mon Sep 17 00:00:00 2001 From: Sudhansu Date: Sat, 19 Jul 2025 07:05:10 +0530 Subject: [PATCH 1/3] eventbridge-scheduler-ecs-python-terraform --- .../README.md | 241 +++++++ .../architecture.png | Bin 0 -> 24160 bytes .../eventbridge-cron-pattern.json | 15 + .../eventbridge-ecs.tf | 658 ++++++++++++++++++ .../src/.env.example | 20 + .../src/Dockerfile | 18 + .../src/app.py | 160 +++++ .../src/build.sh | 29 + .../src/entrypoint.sh | 20 + .../src/requirements.txt | 2 + .../src/test_local.py | 32 + 11 files changed, 1195 insertions(+) create mode 100644 eventbridge-scheduler-ecs-python-terraform/README.md create mode 100644 eventbridge-scheduler-ecs-python-terraform/architecture.png create mode 100644 eventbridge-scheduler-ecs-python-terraform/eventbridge-cron-pattern.json create mode 100644 eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs.tf create mode 100644 eventbridge-scheduler-ecs-python-terraform/src/.env.example create mode 100644 eventbridge-scheduler-ecs-python-terraform/src/Dockerfile create mode 100644 eventbridge-scheduler-ecs-python-terraform/src/app.py create mode 100755 eventbridge-scheduler-ecs-python-terraform/src/build.sh create mode 100755 eventbridge-scheduler-ecs-python-terraform/src/entrypoint.sh create mode 100644 eventbridge-scheduler-ecs-python-terraform/src/requirements.txt create mode 100755 eventbridge-scheduler-ecs-python-terraform/src/test_local.py diff --git a/eventbridge-scheduler-ecs-python-terraform/README.md b/eventbridge-scheduler-ecs-python-terraform/README.md new file mode 100644 index 000000000..44aca83e2 --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/README.md @@ -0,0 +1,241 @@ +# EventBridge to ECS Cron Pattern + +This pattern demonstrates how to use Amazon EventBridge to schedule and trigger Amazon ECS tasks on a cron schedule using Terraform. The pattern provides a serverless, cost-effective solution for running scheduled containerized workloads. + +**What the sample job does:** +- Fetches data from a public API (JSONPlaceholder) +- Processes the data (calculates statistics) +- Logs structured output to CloudWatch +- Demonstrates a typical batch processing workflow + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) (>= 1.0) installed +* [Docker](https://docs.docker.com/get-docker/) installed (optional, for building custom container images) + +## Deployment Instructions + +This pattern is designed to work out-of-the-box with sensible defaults. You can deploy immediately or customize as needed. + +### Quick Start (Minimal Configuration) + +1. **Clone the repository** + ```bash + git clone + cd eventbridge-ecs-cron + ``` + +2. **Deploy with defaults** + ```bash + terraform init + terraform apply + ``` + + This will create: + - EventBridge rule that triggers every hour + - ECS cluster with Fargate tasks + - All necessary IAM roles and policies + - CloudWatch logging and monitoring + - Auto-discovered networking (default VPC) + +### Custom Configuration + +For production use, create a `terraform.tfvars` file: + +```hcl +# Basic configuration +project_name = "my-batch-job" +environment = "prod" +schedule_expression = "cron(0 2 * * ? *)" # Daily at 2 AM UTC + +# Container configuration +task_definition = { + container_image = "123456789012.dkr.ecr.us-west-2.amazonaws.com/my-app:latest" + cpu = 512 + memory = 1024 + environment_variables = { + DATABASE_URL = "postgresql://user:pass@host:5432/db" + LOG_LEVEL = "info" + } +} + +# Monitoring +enable_monitoring = true +``` + +Then deploy: +```bash +terraform init +terraform plan +terraform apply +``` + +## Architecture + +![Architecture Diagram](architecture.png) + +1. **EventBridge Rule** triggers on the specified cron schedule +2. **ECS Task** is launched in the specified cluster +3. **CloudWatch Logs** captures task execution logs +4. **CloudWatch Metrics** track task execution statistics + +## How it works + +1. **EventBridge Rule** is configured with a cron or rate expression to trigger on schedule +2. **ECS Task** is launched in a Fargate cluster when the rule triggers +3. **Container** executes your application code with specified environment variables +4. **CloudWatch Logs** captures all task execution logs for monitoring and debugging +5. **CloudWatch Metrics** track task execution statistics for operational visibility + +## Testing + +After deployment, you can test the pattern: + +1. **Check EventBridge rule**: + ```bash + aws events list-rules --name-prefix eventbridge-ecs-cron-dev + ``` + +2. **Manually trigger the rule** (for testing): + ```bash + aws events put-events --entries Source=manual.test,DetailType="Manual Test",Detail="{}" + ``` + +3. **Monitor ECS tasks**: + ```bash + aws ecs list-tasks --cluster eventbridge-ecs-cron-dev-cluster + ``` + +4. **View logs**: + ```bash + aws logs describe-log-streams --log-group-name /aws/ecs/eventbridge-ecs-cron-dev-task + ``` + +## AWS services used + +- [Amazon EventBridge](https://aws.amazon.com/eventbridge/) - Event bus service for application integration +- [Amazon ECS](https://aws.amazon.com/ecs/) - Container orchestration service +- [AWS Fargate](https://aws.amazon.com/fargate/) - Serverless compute for containers +- [Amazon CloudWatch](https://aws.amazon.com/cloudwatch/) - Monitoring and logging service +- [AWS IAM](https://aws.amazon.com/iam/) - Identity and access management + +## Key Features + +- **Zero Configuration Deployment**: Works out-of-the-box with sensible defaults +- **Auto-Discovery**: Automatically discovers default VPC and creates necessary security groups +- **Comprehensive Monitoring**: Built-in CloudWatch metrics and logging +- **Error Handling**: EventBridge retry policies for failed executions +- **Cost Optimized**: Uses Fargate with minimal resource allocation +- **Production Ready**: Includes security best practices and monitoring +- **Flexible Scheduling**: Supports both cron expressions and rate expressions +- **Infrastructure as Code**: Complete Terraform configuration + +## Configuration Options + +### Schedule Expressions + +EventBridge supports both cron and rate expressions: + +```hcl +# Cron expressions (6 fields: minute hour day-of-month month day-of-week year) +schedule_expression = "cron(0 9 * * ? *)" # Daily at 9 AM UTC +schedule_expression = "cron(0 */6 * * ? *)" # Every 6 hours +schedule_expression = "cron(0 9 ? * MON-FRI *)" # Weekdays at 9 AM UTC + +# Rate expressions +schedule_expression = "rate(1 hour)" # Every hour +schedule_expression = "rate(30 minutes)" # Every 30 minutes +schedule_expression = "rate(1 day)" # Daily +``` + +### Container Configuration + +```hcl +task_definition = { + # Resource allocation + cpu = 256 # 256, 512, 1024, 2048, 4096, 8192, 16384 + memory = 512 # Must be compatible with CPU selection + + # Container image + container_image = "your-app:latest" + + # Environment variables + environment_variables = { + DATABASE_URL = "postgresql://..." + API_KEY = "your-api-key" + LOG_LEVEL = "info" + } +} +``` + +### Monitoring Configuration + +```hcl +# Enable/disable monitoring +enable_monitoring = true + +# Log retention +log_retention_days = 14 # 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, etc. +``` + +## Useful commands + +- `terraform init` - Initialize Terraform working directory +- `terraform plan` - Preview changes before applying +- `terraform apply` - Create or update infrastructure +- `terraform destroy` - Remove all created resources +- `aws events list-rules` - List EventBridge rules +- `aws ecs list-clusters` - List ECS clusters +- `aws ecs list-tasks --cluster ` - List running tasks +- `aws logs describe-log-groups` - List CloudWatch log groups + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| Task fails to start | IAM permissions, image not found | Check task execution role permissions, verify container image exists | +| EventBridge rule not triggering | Invalid cron expression, disabled rule | Validate cron syntax, ensure rule is enabled | +| Network connectivity issues | VPC configuration, security groups | Check subnet internet access, verify security group rules | +| Container exits immediately | Application error, missing environment variables | Review CloudWatch logs, check environment configuration | + +## Security + +This pattern implements security best practices: + +- **Least Privilege IAM**: Each service has minimal required permissions +- **Network Isolation**: Tasks run in VPC with controlled network access +- **Encryption**: CloudWatch logs and SQS messages are encrypted at rest +- **No Hardcoded Secrets**: Use AWS Systems Manager Parameter Store or Secrets Manager for sensitive data + +## Cost Optimization + +- **Right-sizing**: Start with minimal CPU/memory and scale based on actual usage +- **Log Retention**: Configure appropriate log retention periods (default: 14 days) +- **Fargate Spot**: Consider using Spot pricing for non-critical workloads +- **Resource Tagging**: All resources are tagged for cost tracking and management + +## Cleanup + +To remove all resources and avoid ongoing charges: + +```bash +terraform destroy +``` + +Confirm the destruction by typing `yes` when prompted. + +## Additional Resources + +- [Amazon EventBridge User Guide](https://docs.aws.amazon.com/eventbridge/latest/userguide/) +- [Amazon ECS Developer Guide](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/) +- [AWS Fargate User Guide](https://docs.aws.amazon.com/AmazonECS/latest/userguide/what-is-fargate.html) +- [EventBridge Cron Expressions](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html) +- [Terraform AWS Provider Documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) + +--- + +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/eventbridge-scheduler-ecs-python-terraform/architecture.png b/eventbridge-scheduler-ecs-python-terraform/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..58dea81ae0faa34895ab8031bd52212b6f77194c GIT binary patch literal 24160 zcmd422UJwq)*!4jwxVr7%%UO+n1IT`NadV!87t?UtHq3p85I>#X$&alfEi4UifEfq z+lUFZ&4MDLqH``{fA*XAy_s46ntzS!QdQ^NefHjG=M&82b7^f`_h{X$S+lkbI$7AP zSwsx@eWumV;4>#@C%IX(L9M+Mxz`rdo2ek@ zhufK=vZ=xjJNRt_F94Lyp*87EI_)<|1Pp;rhGCQ8crgr~fFvNH4-_s1jYKNH!K(~9 zdlOoYBW$u-RS5_xIt2z$4dAFWCcD?+F(x31pv|y*byn~Vy1^$;1U}^84-QL#qrXA8 z{Qx~2iBEwI1TSbhjl}`5K_K9PgD$Gct0G87fDHg5x^kmL6Wp4mBFn7 z;(^&!wkB9Un07K?2tsXYl9f>jH#$+6pd&z3k2kp81e#Ovuh_ohv;}EymD9*^Xd5M> z4Sr(*jz9s75K?WZsRxche(N!~zY}Vr3w4N0;UC4DOASfCdGTaq3^=gX@RVp zns$dBh@7bLx&4ja3OtkOcKGbt#uhMKqQmVqIt&iG%F1>)oIsEeh*{_LhMLSq<5ju8ciWWaFH_&36A>hd^zZs3!RQ2$m&O>lsmLDytb{}!&%t^Tux{Fj~3bPk&i z2m;uOTW3{yP5yr}s%r8P!{2a_=l$>45rAnj!~e_+6t@4xTL0~|nx^kpS$$0`ev|DF zHsi7Ayc)<2Aw4;KUaQHjBmM3C-<3ylSRL+0IU^BB1Ql?HB!Z}?H36-Ef7fJjAhB4# zcO@W@6f7K#B?54d*X^(X)ZY*_0c%wr-)ukIDNjB&!`>FUUVg z;&%Xo)ntHTwHGp5qN=H-0leq{lRp~lzZza+yU9bKxv4zPZ&6Nf3POAkoeq$abZ&|t zAcNEh`2>hGjY+^31XFezQ&a&DDn$oDs6mW1=*D|AU=luSBk^wnhB(zhzVIK+_dl!n z- ze`_dK$byI}jm6Ly^1d6LR;O3_tRN2jjp1J`>0hm(ks#rFFJv%HEReM-uPOma_|`;F zopwV4f@G2sdBOmUMKd@E;2&2cF;XN3@QV($i5L<=0euH#61@fdAQ0qSkr2ipxIHKh zMgZDOqE$w*+QbqVfd!2t7zjiIg+?H7pazjZV1O@*=)c+&sI4LpB;Xr*PasGQ6!nj7 zxq$+R|1h4RbrJ{=v2XCoMmkWF)Y#ehiV3x$_dEk=lN(zCs7*2emLQ_vP|#flLg2@C zLV3<)X28m$c)0q~f*(nlI6o2E?*n~E|i0RPM zg#n%k=hq^&NVZ)QX4~)~We6AKi7jY$m=I!z86mbDj5oo$!u_xS;Mo6-z3spvwOhV1aO5oo{% z)uKi)t?Wiw;bAJ7(#8%`z`SrKzQOS~4uA{yZM;(i==;@lDS*ZMq%yQqOGbtGq;F$+ zR;E>hz*O`IzKg~C51!>50vBt|lw%0`Jp97B{i;=eKQft;;s zyU^Ig*98Z<~c=Y!3K!WGCnYRp&E=}Vst>& zWGb7$5K)OlYCs_(kW4HcK}f-KBwC8W!9%D$#oDA zCW3;;P$|$33c^HIaR>&5(_uycKnBqfFfq+G(EGnRMugGG%mjo^LZUK21x+PMBuKpg z{BWyX2p5SbL8ApIm|Ym6U{OrFLS$EYac+3f$`+_7I+z+PYpFaO0k1bR7;GhuhL8pP z3at&tRid>-shk+LhXpdKOG9^?_&gz+7ewNyc$C#lK)Zq=ITj`7l3fP25@W*=FbcDW z?Y83BLYLdaqNDs=huCeWfN!?P<6#?t^l(lXoCH%E{V17PFGeenMoZXCP`VI)tc^#v zg!mY~G@uRQX+8i<_24yXy4RwCR87$Hg*dv^4VPnmEIcwOK%xRL1<}h!QaK`n1}~E0 z&|wtMg#oZiDNSp^YJFfaO{BSGd=}0t38U>qBFm_-+teP=%~7(5E-cL-&>3+YsPd(9 zf)>1%AOpXG*nn8?py^G1&=8tIKBuak4DhMZA=MJH!KLO3@E(CvL60J-e#FBtdvbx}8On^#*xYCEvZVq3j zrGwunHWCJD)aKV25J*1R&A`dSOf(TI!i!}%33wy(SoM6Q-++^9S$v5`CbrQDWRwD> zkcbUZqr&RdASs}vqN1o0hn`PRTG{XrPsk%8WeS8jtgtxzJgQsA0o+*Rdb*EcwfXR{ zu#P~3dl3c()rt*~0)VDqbqYu<6$gu?c`;}bic1qq$m+11BZ3?<OK)ALs)YcF^0HaeJdhiA%ViLG&CEe!Zpea%|hHKH- zeH5hI!VXDIR23I(@@km)FjWrYQE_xB7@;?+8Fr>ttqFNu8oz@HqvDuB1Q~`v_#L#c z9s*I~Bw{qz35<`*Cy1VZCqMhr6m$yFba>rEQAGhihP&@iW-ZE9eG0jmjVb#~_N(ayM7V)%XM)o5XA|^JploMX0uWoDPqMB?oBC z0tJj@w+BL8sU1{xM5e_9bJ&m)Bf-n#3z6Y4$z==pIc5qYV1i^4v{Nm3pHbnpfDq=z zXqajX-DZ;F5Hts~J5E>&DdE9g{n`xoooOZH=?Zh)N47C*H6e`I) zi^;1CV{Kt3OG{89f#VXbl(5stCE+*{l0{4cY=n71dKgc_s0BV3kKw_{iI}hxX5|Se z7_>a3kg1Vjl*SH5vUGYf9gnvXr8>46g~PxR9w!1ACY6WBtI)z9?~-%j zcr`4DfswFgH<$`EDdf?1d49^>oNabXVT+ii_(Q+hHMKRl~il7PZ z6Nc@685WR51?VB7%dXV`qa)c+Xp-E>W>XL<3>@ykLglfW!1nTa3?Mop2aOT%;5ZqX zss>?)s3Th`LK~Y-&~k%9ZivOUVd*9tn*x)lVQdQqt+)7;3?>`xGATKB4B8}dBZ){7 z&8K&0nF#)MyXJI5nXQKgw;5u6UvuJ0WBAs`}GtX#?wJa0BXu$K86f2X2 zG)ZV43GiAPpkSnULpTlBWkrbiDx6fu6LzOAihvrzI_hK}bMqu!@Fs%s#kU6zM3k}7D zBCOl2A&Ut(A{NFJTA)Cx2~ZhWl!+Ergdy$&el|xXF`5;4G*K-IDfvu2*XWl8+z2bn z#MXx?k|0*da|5Jw0}Y5@4L6H1VX(VJhC2fR5dy{x!5j#r5Dx9k6WyUOU1_1HlzajW zV>C&{7#WViCvyZ!$b9ugu>z|!A%%3eIKV?{SU60WWppCQXr)oc@mX{#p5I5r%Wz7j z6USjO+)9McYJ$7nBBjzJ=Gp=rfZk)X!(40`nI^*u_*y7&Kt7|DdJtllMCTK+6nu^k z8F0doYz`kGHnP1sy;XC_53X4aDC2>5)5Y`t!dMR``L+jzo+)OzK z6SA3g7_!(ZmFt;OE`~03+kupcR1?BP#n`oKsmhAh%MCQIPcNXkyaZ5w5SKI*39V*X*Q&B*fuRP)?%PW>R7NKCjV^fyldwY>*8c4uZgj z2l0W(aXIaBd6-Am5v3-Y1uFo1>r?_pB@ZI4I)V)Dl^g9+DcsAzSZ!*!fG+oFH4LH? z0}ffdZaYmb$D2)l$eBD?yqK>gpkX=%O2`z;Lu628Ih-Ch%0c#6-BJZof)4VPSUsD> zBf@b6goYtBqSSPc#!9!z>^f@DZIvN8UKyloG8P858{}{$iGcB7d`^rYu#ZXBP2HXc{+yEWhG#|61mpn@nO_-ol?f)k|3)j8NCRM(L?0Y z8E&S{AQWj;3Zy0oN06Cjs0txaF$gq?3G=wPW@ZO2Qq@8= z6bHaByoJvvV04iCfro^`kaRASON0^-@!&0ZL)==`hs+lH=y;@*B@!XjCRz|d7l^GCT@Z=INWE|)-WQ~Zkuq^mMAyRUaEc#e zp{og6q`{AL7)cH--a%j~X>zlIL}lWXFn<7p5!ftjOBgOj$ZQIonW2E$bOIa9UDB#+0TqYGx2*6D&lbB9|VM2Vj z$jYKXXO|?A5KhEN5ndyaVv|WxD!aw3m0IzZU?`}-(aElWL8pPkC=dla6BY`FiDs5d z9I^&fZXXHl5ex8yfZWPASuLS3TEWDLTml)>hlDf;hM1@!D^!@-87P*}irZ} zn8na2p?X0h37gq!uY=|hd(;}g3@5XM6jV6@C8jAM$EwGTSP9qO$!g=0|dW{ zYvrh!JfD+F@WWLU5tt)|=wTUL;y{pxcY}3v*y7VV^f)w`C3F~sdcKv!w<^VIp+8LF zac~HfNlcT=bRr2jSto)F#nHNGCNcpoQ!Aw`4OYcbX$dSiR4P*G9<~Z?ce_YliJBMi z=y_tcO~b@kO;G70Ra@|UX#L7D+Wj!8NNmTd$ru~PYca7xD#&IDY6X%V5@KL5B#B61 zq2OT$*@WPe?P8@nh%+ng#sE0O1gimRSjS{Bi3k@6gk~+>X|==bb`&3>;kW|`We|;k z0uO$Ovk5Skm&Ed_L0Ld0qx5z<6BL_tDc)?c(xDpzJ~ja@w-Q_&hKws# zVtH0q<0b}pLj!-{aVgOT3etoH^(PT-)u>EXC;)4{0xFE+(qmb0Y(U~8AObj?%w!Qk zVP5GYyWwFXPA<|rST<*nF6D|58XVs(2iFdWZY`RP#W~?2wgy@)P$1*vYkhD&5$$sb z#a1U9As0}+B!dWlL=k)0G;M&6#F2;=f!*wf zf&~?(Q3Bh=5wKc6-Pu@(Smgn4*u?Q0ePHuTMhGkIE|yx4q0t=@Btz}Q$nDM`*3UpN z(X=p`!MCHSJ{Oy)F|*((NHsX30_<|>`64-<;DS3$YD35dr4T^OW781CI6SEQab$|n z;ec6L4yh(6kXex;2~49CqlIL*l%v-%Av`P#T2IsSVSc~Eh6M!%(V$gHgfhPn=@V*I zemPz$^T^P2sXD}lp)CkV*>EM3tTN-oERR_d@&|+*uw&x%xkP5Q)8nDotr(g`DnvNI zz8yeEV8h7_8ePjUFhX!Fl?>3cQ2lDKD`mG+j53#)0Tqc96OZE(a_J#0PHtlI zF$NHwBv5=}2aE==s8DevCM6zWQW)TRgA&8kL3ZO8hQm~wk!nyY@H{*LYe6dzbScj# z#K<6%Q2N0-70m}ntTv@jgq8R(av@gDwqWfd0Y<4ZVS%xkxlEZ&>;x-GUD%>?%e5%7 zTSO+Z*$N#3Br6pc>=y{xbRpk@;OLzwW5C3qV)(Qmn~QT;^%lL%EMwZ-Hel35Dhj2r z%bY$59Eiah_EUonE{x$a(RE0yNl52eDRL)EO2bhkbd^EqMx%^Y9R@633~o3Vix*N< zP-Ve$`Pm8(OJQ1>f#b0GZ7!TUj8ln)1SO2bP|KM}3)cdiR%4@wxGX`4Ov0(yGBwkS zg<)Y-rjdj|1o;vfP0r%NOk^?-XV#hh`k=@KxgFo(WKu|e0jDvWVO8ffC`KwcR+AIyGFmwr z7LTb4FufFxkn5lz3}mo_Z9o~FW*fAb4fe-L;Dph{|F)TJCNaPUIOqk-4uXQiaDfeQ z%YQ$HK_RIXMkfVYGlH#OB-c!$>6EJAx1&apM9LGg&?E(e0Ub+!zh&`%_n;bu1>phc z0`~VoHKwWK$AfAJ0s{KSLG|~}|91|mLE?hA{NbRQAOqz&jAV8AKvIF;{U6<{0GA2= z-TjFluG<8RU=vK_R5f0434pr?KyCl48x8;VvI2Ar4h=TA8Q@|U(EmTKGBmy&4{kM# z*ECM$U+-#wTO2qvj)+G=h#coy9dm|WH+!ITVy13ln%}0`^o%| zB>r(Hq)F!gcdt{R{>4gB-!Gm0V5Mll^Z#U}i0^>^H?0&2Hp!^}%@wxBcmKF8_~WgT z#<$<^Cw_a;GyuB&`R!KQ&YaA#&6>qEV~`2r;JBBoqk^3%A7>rSVI=1C@85q+>jCj= z2UND46FX?ooc+PnHHYU6Zha`_qpY;{gyy71a;5X1z7f)DqO4PK8u?hy&J;nX$f_gm zMd!4L(?^fAL$WX27<&i#^l5$V+bR`SBpF*PUD!~tU`K3#yQ)*-n7C#UO-~%-SXP^y zZr%@lQGdOev{*vMbZFN6+q0$Pf#*kPQ3%!5NS3R-?QKg zt7i>|7&z)=Klxx^qhkw{IU;KW}_ln|0Pd$vD zn@?A<1sk{4q^NtH48v=WKTis-e=xoMlR@i#u3p|HCun*$T0H)48h0bBjJPP}@Q2|W zRxo(O!O?NH_xQn$6*bvg{yLgkLKybyVov$VcPDRb%9x8u&_rqJg}^+ zd%C>*3Sh(e&kvUwLy@xb_GV0*zoS{4%GW+S??Bn7{S}} zW>X8^t~cJYd3E}Vz^EYw&v(=#tHY7Kb6WjODtgNuYpS~wzP#qwcicnvcm(s_ zXHt6GGUu^BKOZGBvc|6Nu<#3!|G8XTo_V?#<#6pYM*B=tmu0h3RuAjQI!#(Qf;0I` zMXT1cKRS=}9u`=)`p`@I+fk!`o3}YF5j8?ck8BJYi&{o5E|1(X3s*O9i78V)roMLn z-ML5KPhXiUOum-~N3zC0s{}=72lggHBX5PD$ zwS_j8_%-xJQe9J-@v)D)8E56^A!A)n>)+K}+28r;y{tAz%qNS^-sTUCQCKA%nKD2gk_f4(vE?`SF^acSrs@(4^=%=gvv*V^h-l zrI{IbQwvg$j@wi;H?_;Q9Y?;jL*&9wx+*KLnS6VDo{?ZFEyE3;HJ|Fi4^{j7~ znKi9SRm4G$&wX08pkqa+S+&FX3eE9#F>5XhH^h$n>3F|Z!%sN3mQ603`iYubMW67hrU1z#ir*^F9xvO8yaLMm$ ze+OS5J`mOtU;mG=}i1K64yT=b3{9(iA zPtVY$^`RNlXp|3Y^3v-$Ctl4xI_d*YH|+V&d>S>kv|n8DiX|tj+8gqEG>mXWH`IAj zD>ih^WA((^r3LuyCC`MEakx7v!veC)BdcF`Ep4~tJgRDT^EqsC0c%;qzQyAv)^aF` z`pKUQ<73B+Iub0M@u+1*>~3C{rjRV3pC-(?w>-Q@F8;v6S&$$yDdtvI^>O*s`Q*yFqmW;Bbr0=>nT4$d5)_Oy@(K`Ln zrN}N*uA^g$H(h=>aKoO`wY)AXlD@uOi-@S$VrYNp)0fkmUavV^adBPCfpbx^Hr>@Z z_iDyXYOv{^oH>8z@MUCbae-Bl!KRN=mbGehlnL+%hHfpp7&a@#`JWxVFWsB_ zaNL*y{+(}n#Ekp(c;4HSb73LWG~F~BD>z73@mTo?`Q_{I+K8N;*Rjp7e#<6{5*N9# zEqT~C-+as{%QpQn!`nuvwRSn2d~Mq;`i3QhWw>j&1$K?71g@tM2MZdKBGhS=9HcV?t6y z_vzAV=VS)MvXqhB_2|+iEe@3LWg80zU;dUMz-PClMS{e6ch~NXESsU|FKE|P5yU2jk~|YN9uV6P1=|y}8TS z<$H-Trx#eD#&v*kn;}%I^0Qe}*kyIx%CqDOWM0L!%ssQm zmu8*4v1`?!Q|>w0lDOSBR#?fDH92ur1u5nhLjl_<) zA*MxU?m5I}hhizZ<+Xp3&wM@Oxbkt&JmurB)ceaOoSL}6pHTJc=-dxa3qOyZIH7J{ z2uFDPV*Jv9E0?88`*Ri!Y{%)A)hg0W1-^s4VsrN^n=#xEmI>5+<7>gSUh8A#JZ?WJ z@vi0WV%g_z2V-^inpFq;WUtwk7vH6D`{VY@2Uz+mkIbNVyFb4;ZV?Oe(97o=O5$Q_ z&e{6K%!{j7N~;w;5uDjMLQ)jGLwTt2WcOKf0`~kt;i4MBPuY1Nu6Lbyfs-_xkTtev zbzSL|nTnDr?zCfLBK*su>ZbN9^PE^s&ToeP>=}@ONtGOb8#}pWcJF#w&E&eF=n~(E zjOWsvHh)HJAS6}|J1#|J*=MY+Ixte~YPcjWzOX=`tn7MZ?WxmG1ot-9)f(n@A})ey7TK#G5RD`ow`E3b||97^eL(c|I#SgpADO=ANmif^i5dGt9Vyk z^(k#O<(y>1u8i32@sF_gA5&|NjG8*J&xo{6okrK#+z{={e#G$s4`>5|yR?5ipS*T$ zoqLC@7K6~Nv*1;Oovs@{xR^7~Wa{kKPc?HRgTBB?#B zO}%+-I@>8!J^90)`0h_r*3=jS&#M_35rAk1i0GUt$$;qk#iZ1P3z>neJ;fxU zSE-9%eB*0F?v;0^#&;~By050{@3chp* zcUQM|6K(RKVVaJeeX@m%KXyIJy1%XGrmA~yJ0o(mYhBNbI`QvMO??u&w?i#)TqD<< zFV5Yxfw9LAo?L%#`NaAa#~HM~Me&a>wH|lw%;=G# zI~zaNx7nnnJGM~@*r#2v9`^L4`<{004YdzGkKw+Mj0`Vg9=n`<^~0Ok)}MC^ve(mF z#d=CxMELtH+JU%l-2YSdYH$04?T;*qZAU8_JuP>cpyKQH4a;8Njl1C5KPPWA>P)1^ zxilc8VwoQs1nec&h0|kh@7}%lT*i*aW9vwXRfo4u&`t|*tzCuiUQ(#;HeIS7FTYqQ!R>{yE+oeBIhz%N|*}X>Z3^y5)C5T|)o6s)opIlmYbzS6!dG zF(YWEojo~gakZHD;$-OHFJs~+U?W0FG3Vn4b;qqNUvM-#c0=~1SFJvkRITXV+japG2Dq1(|683B|J?qwb`{9R{?nT>R@~PEnj6y^6_Tq3#-=;dHl<#XG6YvfzKS? z6sKKV-;ifK+X`M1Jvp<()$;N8dvL$7F%f*EZT;@U{q9_?xyrmb@ur9om@ob+V+Xo* zYde))HtGW~8bo9bA#v5!5u08t{dA_}ZuiZJKmXZ4pRZmYx3ho!O1K8Gi!+e9rOfl$ zCPv&V{|h7h(*Md%ZfU`Rsh`IW@AqZA{O5FTO2q^{)F8SE2Pwba3gD*YvAcBfa*F3U={2UpDbwY}g@>dwzd^Td~f zT7UVn;b1R~&;*=z(Z1!G;#7Cty>a5V&k|Oi>7EsJgt6JQZ1d^!!V`|9!$G)V_rS&U zoA;g#y*aUF2_u1K`5lut9IvX2+RVcKa6UP-TDXp3_EA`?Q-Ir{H8 zQJffYcSrtwQg6-y&d|d98S{tq^%u(%%Tq_KDyR0gpMIRr!6ZyT=Q#a0SC?}quehDQ zKos5n^N}elOIxK)s83q?YkW^c{_Wxe^M;?E7qc!=b4DQUxQ(~T)FyPdz9;&^uVc@_3F$__B*F$CiN_P7fda|^w1&<|RaG#u@fe~R-U z^-J!idJ$aRz1>r@{hC%iI(b#E@W6FRuZ|>bIccaR?s~34wODrklH{(n|4eq7XzyNj zaD2nEXVj9#X&XQ0L`Supq$3(S|j>jJ-=J$x|(63oJJ#F!u>rY`T`^FUitWM86Tsl1e@^u72HC!Ag=L@~@nIRB*Z9 z_Jj+&9-f>$_w0U_pl6@BpK~sLx?<%yEIId1W`4PF<=4W{;KcUZo|l|k6m`UaIDUUy zA-W=EI{UAeoz}OU8XIG5lVcpQ$TAJL=(+pq)G_&ou0BYue}3j@oU2ImGmEQJk8aUR za;J2_*>th{-oed-_FrwTUKEv+v=O~S9Xj2aEEg>Z^pr~OLm9z+m6l*h)TzY*Yd8pm!7X3Euo0H9co`~ zvAN8@|4|Y1tZd`D@OgY({)t|+@CNP_wiGoazC$T%hAJB z+};c}`Pt~A=M#2izl$F9?9h(QQC}z(rG@MNta{N5UN+-%jCOQRE1PwFZTjozxr?qe z7jAwvYX0!9v0LYMJ9;HMW7W_dT2*2F?FAp+qQ>0GU=N`$&1COq?InSO!k4vc$5!_} zI)7#bH=)&;S1oeNuAKdFn;Gd&hOsGwq@I%yy;!5mXI$!`mZJO_h6KQ{~nfEM^QM#N35A$AXy5DO*Pvpu z3zPm+#N+6)0`}5h4y`Pod1npa>8-Lt*>T6{0izxY+^^HkMaFjNFLP@HSGR3W+D9g7 zXEkuj1rLw3KS`K=t6gj4;cPKbFm zJ>|DK(HAz~eEju2+T3w+=IK6Vy57xZA-3`T5AF<_8g+I1+K-Isr9)fPnvSiBc_p64 zJ2|dA`tjNY$#+ZaKD ziSObbyv3?h!7c&vPEygqfvScUPwpdWMdOMmO+TAiviRySNoaoi3;D-VpDjAD;rI9K z9;bgolWa@X_D!6eedE$f86%XB~YG%PRK^!Y_n;ejXbTNTAlI$XcxOx2?| zw+3=Q40(p9<2~y(-TJcGT=aRkXvosyp_bG0hka!H2#Eyj+_N^H;c<~fTdAF?LL z=bwy14qNpwJBA@kT#q~^uZD$pnT)4ZIQ}$$#sjV%&XoYls=Bn;v z_OA#&i`)6);ko?}#wN}hTL7*mNxkz5+P`%@eA_!_>K=PELYR9nHnY>N)KAuTEwse7 zqJ6qud)fWlDzg(O?j26G;cI{1c3EovvVI!7iZ?#L;7pYEVT)y85h=*1*^{(n3oGws zM29y^9FIp-HQY;CJ7LduyX?XEH?D`X#^;Z;Pkc_TIo~a{^W|Zp7I85-t@hq|5(0tm zB5Z7NhJ0cI$)99+()y81KeGa}Ztgqq`EZSN&`$`}hRU?D+E!KT*aJk7>REKg*R0LG z;*-xlo_lHb*#SkbT6XVrX?VHoVf=7^^()$z@~EvJspQA8>ug0NOX>LzruF@@UAt|mG4p0=NN>Ef6A9xUT)i+pErAs7xo$<`e#9Z?wTh_Y zdvI;?#;vq(ZqYAB@w`i?Um}E&TZhC*utO4MPhN`lZ@V=jHKtA66XxElMeKq57XOS* z-f(ExuTKxP)e^gIUzM2BDrdsl!h-IxkL;U6wH+VCr2TOu*tPsIGLPG;^~t(Rh(987 zV25e^-mSXzo1n{?oqj2Nw`8pO@J9I1r=DTVTZ2b4SS=T3CSExkgUI@$H$v(^UPcD1 z>`nKVbuVo><>dH@<|JNVX~R>P2G#H2=xJ9zd>CGhl>PFsa2;z<{H4iV&4F96L6t{G z-~6On`TMF@X#?XXbZK6aI%-=Tx}Q<9Z+wyR*5ZMyv%)>jK*NXEw44{vMW(+kz_ChQ8*?>=3<%UC}{Fs{#W+We%F(kskAmd-63fqVO= zcV5ZyHuRTnG-pko^EUn(`}&4Jn{~YwMLle=l3UHIx+?TQQF~ETMzfd9~zR&*oXL>o8|BaWS@Oy|Cyk zu=R#x_ajc|zhl`-QeqK*8k-zt#9vu7W$8hl+qaD!*KN#L-k18bo9atW zmdEqjEzW*5=-ts1U{7njxT@%9u<=zr2^JqotuYtkwZt|FWi#e9+dO4qJ8k}9YAuGue15Q_ z(v+hkj`P_JxlFE19NDNKu?miUu|MtxAC5@7PNc*yPy%462A0KXA%LcoP zs?Rrm;AKNZc2wobUA2{pD>aW=>pms%(50ucTWzz&y$0JQywAoFY<3%a;An4g_|&BM zLywMB8Iz9wCOTVElhF2r=?RP1nL9$ZRZb9Xl?EYwt=Jlk=Fq+!{d)v zCq%F?VpqwEX#`&UNqm?M2mF$5lCTOA-CPQojUVyna}*{^PvKc`3FR>4@wdwQH(%1zSe%-nU`% z66@F{S4ZzlE_^n)wu2m1akA@pwxguBi|uAnI^2&a1g7;m?FTC#)GX)T5<=&!5B)Rf zV-9lq5AP6FPos`BAaXu99(7!KExi|5f^E20b~Nj2uTP&opbAGmyt|Rr^VdA~BTI>4 zV$RcN%eLAIpDo=!W?S7A_o(FJ2Nk8P+}>~`>;2`-9BKXo@1&!7*S0E_yt|*0oV;OD z!^casKI!B|QT4AU_um-Z7q(c&`lD}*a=?u-i68hwDE-MD3bnHOO;fk^>!_1oi2Hh= z+u6)Jv8O&Lk^{v>Pot5^HSvRmPpbyrNUslQPA}MygEU-TmOOgx3OFKbb<+9S9lD*_ zvH`s)DtW0jB_?_NHZk`v_Pz5D0RYzTR-(jOYIRD7y7LdfBLMH_9@zGOc_{4z(4TnlNba;Ox~s z3;V~r+n3FVzPT+lztz;NVx_rV&P8Hjnl?VZQ{~r56iNG4%Uqu}4j)=|h1Cp^zr5RJ zLvFM93Hn^8x_(!x_4TYeXzgCQ(>=Wal)xyJc?mgir)~iL)hgV;;r18VHevHEca~}x zrPOotu3Q-L+s1;;?*5!@s)KkcWnzDBGWLFLUsc||0jrhId;NO%Ub(XM_UlJFB#&J^ zr>;`qS$3K2wiI1-!qSf5+izI)aSD6I z>ANo$$Hgp)Dw}cR?x3ZH(O|24$>DwLdxd6J5;siWkvPlpDJ!zDU}RiO;#=%4Tv?Z7s6?n_;-_ZW2d=%P%Vzj9!kP4;tFBG(Svn)h2KA`ewQj94^gqWIvR0yax1S$WS~ zozZ$G>JiI-E?h9LxL<ijTyc6dAk$&9qNyU$7Lq1DoK@El)oJ_q)uc7;+3)r8pG>*^ zpx@owv@_V}yxjNPwD&;kw!ncS{*2jHW$7@&&IX;8h7`_CrYO~ zLq+vnUb|MsBPhdqZC%#Nv2D?c%&JcxRIZ@#c^I+nF!sUVzg`q?jAwqy5DhGPgcUu% zkO8&}vxxsXml{gqTH&rw{6P=9em*~I)A%>@bI;V>FL}`7Ybyb3N#FC7k_B-Wc1DSd zpQr6TfN~_|cfok=m=8#Ffi zN=`RKevF*$1~yOp^7K+p3}|*+JbFRek@6W4m+yC)+x>!VY^NKHlFGOPx{TckSHGS_ z7MqdD0dVHf3GS*Jp-7HyepLRVe!cufC@G`owZC2slPy0Y_McimZ|W9r*RB12(h^&4 zJ+L15Lmc%XJMzeQQZ05%`_`A66ILX0W4Q-6X@wm))WkeLZV98S1fruJ4z#Z3E*+bJM|Q!QuA*qn&e)hdO)X z_$1bC*Ikn9j0{ClvdvJ8xuh7AG0K$tEo9rqb)v{+y10)C83yH&3_|5LF6G)RDZ41! z$}%V+S_(;K&y@DHzxwz0`|tPfdA;VG^F8Nzp7Wf~^FAZ+1z(mpYfILOdpl4}loXNW z7Me=Ce_fpj>PJkZV6US>RbFqu&S>M~b3Hp#?>PGmGzpa|F{qtA{kJD=WXY!;w6A4+ zxF+jCX;ZW*Zoj=8Zso6^kYxR^+@D>N^6bx&$lpfFIvxQ_qiO+1NqzHnq~G?vfkNQ3 zY*dX`{@cv#vy*hXCu;LUQer?6pM43#&pfjN;_STKx3OX5g%8u{1t6CHd44v8$ zkdQYbalU^(^Z18#a%(=(h1b6Y( zr{^1pcd%((^(onyKb36z9QWFdyj;3Xct(8YA}cW={Fo76xU|4WV3e!G*4B<3oo<`n z-9^aFtgO58G<46t@U+cLa#s2WPztCL7_;EemJZysIr^txXfaiTeI{tb@fDivCt3d0 zYn5$vyoEA(Y8q9Jq_H=C8(oh}bYc&sU|bgt9vZ0)JJxisfUS}qcQAJrvr~?={n2Do zt4DUp5=OR^huLN2PSNw{d?(l)CBg2z5qhYta`Ug$6ytZ7KlJAOVuV&3!C7}+bo^8H z#H~}O&kVG+RV$QO_3az8oR8Dk;qZRa!oI)ZN=6|~Bs0qpntDOp>j=E2+EE#bSmD#i ziO@@q5i**h{wL)O-Z`oACT>mjr7k>TwCyAjY?54sr(EA|GPY?j)8Q#ZiveAO^=U=_?a1|M zaR$?!}sL3czPR-A?bu}tU>O!}V6dA*b2 zLHZ(*IXNX_edd?&T&)WN8^}}7K|fP!Kry;_D-$MD^K1D z!-7M`XC%9$6H4fENYYHN?r;d5oz>FQ-N8xB=H43-Q9ti@1r9o^!h9XcfIbT|j1N~k z&z`P-4nVKW$GUuAA5h+E*YM(AHKg`G&+tx+>GRDR=Nf4 zWHaUkwZB!tiM-lZV5H-P)-l{#tGYptixrm0t0lq---W!cyt-nrYSW#zS*FW%dDJ%B zDEJIJZp=4LYKu)WjC$8>)J@Vj!DwIO+;exP-xko*n9w(Yoq^Y-EH@^_8rZiwHL$5i z|B@(MBj8mM3o zxgY7`5se-s44gNpoMa?>Yk`=6n6d7jm~py}Jb?Kr3?!+Am4_XNB9|cVt{$^)iqZ4O zQCK^UGwiAm4`@Y z0Z)weeyt+cfrlIS3TL#icD}xyimmg^94`7iLxpZ;ED*4DS!@73m6i^=R?=kymML+%o|a z?23g&aAV?9)jlPBbpD#APqZwT%Df!x5Rm8Ibr46a{Rqz652nU(M&_$|e9QKmVr(?an9YS5}LWe99PYhYl?oV+@u>5#j8*y~Ohfk1^mZZ8r zV}s=6tkiDzd9Zm7gXTJ+L4)^FUJs+s{?cAE9Jndrim8EOOVQeaM_p?Dc2#RWyx2}v zg3sHfE)&VB&X~s=q2~+AIjVqk$@#{C9lNl!zyK!MKJ15dsrp=2E?ao<+*@P{`i(EP z&b@qMr87|ij1DaEt27TtynpzbcAccz4xx%RR=_U(QLy@i-xlocjHNt>b-#;aD#qZs zeYCG*n*va2u9>nqahmpR>CctxB|DziI0J(6rS#XaKzA#%Qgv0;UTim1S{WDy{d{=v z3mSe>_~Lkm7T~s=u4W#G0-c`lbt(jOp+IMklNZ2@5#OXyTata1>5#(<(+^8+}66y<2|>SHR;y+X4KhsUaZxuU{RX2FHz$ zD6`iPOhpN41h6$P80~R67U)Ao!-n^Ef?#~(^Im$Qf#-}z&CkM2`U5HE zWzUyq$4uF3?-~1uQ0ad(kdP$E<7(_^eQ;kRBxy} zClzM|V#x1M9kD1>H2BEDhPKb78Qtd+Yn_mT2xY#wZ}iU_3AZ%J0eGSuHgX-P)1?3) zNa#GI_uP7ep%qx9G|l;S#1IuNAB9&~-Wc`b3v);Wx5)zGRE>3{Pl5$L^1cRS&zFWo zfb4D7zP!3VzU>I0%tY28*WQ0Y_5{qIuEn_MI#ej617YgE5ry=uH*6>XKR6%`Ghb%} z6%b(2qs<|{X6p@`0b^xoCG&p$QO33V;5IC=x&gKD<&}mmkR*vQT= 512 && var.task_definition.memory <= 30720 + error_message = "Memory must be between 512 and 30720 MB." + } + validation { + condition = can(regex("^[a-zA-Z0-9][a-zA-Z0-9_-]*$", var.task_definition.family)) + error_message = "Task family name must start with alphanumeric character and contain only alphanumeric characters, hyphens, and underscores." + } + validation { + condition = can(regex("^[a-zA-Z0-9][a-zA-Z0-9_-]*$", var.task_definition.container_name)) + error_message = "Container name must start with alphanumeric character and contain only alphanumeric characters, hyphens, and underscores." + } + validation { + condition = length(var.task_definition.container_name) <= 255 + error_message = "Container name must be 255 characters or less." + } + validation { + condition = length(var.task_definition.family) <= 255 + error_message = "Task family name must be 255 characters or less." + } + validation { + condition = can(regex("^[a-zA-Z0-9][a-zA-Z0-9._/-]*:[a-zA-Z0-9._-]+$", var.task_definition.container_image)) || can(regex("^[a-zA-Z0-9][a-zA-Z0-9._/-]*$", var.task_definition.container_image)) + error_message = "Container image must be a valid Docker image reference (e.g., 'nginx:latest', 'my-registry.com/my-image:tag', or 'ubuntu')." + } + validation { + condition = ( + (var.task_definition.cpu == 256 && var.task_definition.memory >= 512 && var.task_definition.memory <= 2048) || + (var.task_definition.cpu == 512 && var.task_definition.memory >= 1024 && var.task_definition.memory <= 4096) || + (var.task_definition.cpu == 1024 && var.task_definition.memory >= 2048 && var.task_definition.memory <= 8192) || + (var.task_definition.cpu == 2048 && var.task_definition.memory >= 4096 && var.task_definition.memory <= 16384) || + (var.task_definition.cpu == 4096 && var.task_definition.memory >= 8192 && var.task_definition.memory <= 30720) || + (var.task_definition.cpu >= 8192 && var.task_definition.memory >= 16384 && var.task_definition.memory <= 30720) + ) + error_message = "Invalid CPU/Memory combination. Valid combinations: 256 CPU (512-2048 MB), 512 CPU (1024-4096 MB), 1024 CPU (2048-8192 MB), 2048 CPU (4096-16384 MB), 4096 CPU (8192-30720 MB), 8192+ CPU (16384-30720 MB)." + } +} + +variable "cluster_config" { + description = "ECS cluster configuration" + type = object({ + name = string + launch_type = string + subnet_ids = list(string) + security_group_ids = list(string) + assign_public_ip = optional(bool, false) + }) + default = { + name = "eventbridge-ecs-cron-dev-cluster" + launch_type = "FARGATE" + subnet_ids = [] + security_group_ids = [] + assign_public_ip = true # Enable public IP to improve connectivity + } + validation { + condition = contains(["FARGATE", "EC2"], var.cluster_config.launch_type) + error_message = "Launch type must be either 'FARGATE' or 'EC2'." + } + + validation { + condition = can(regex("^[a-zA-Z0-9][a-zA-Z0-9_-]*$", var.cluster_config.name)) + error_message = "Cluster name must start with alphanumeric character and contain only alphanumeric characters, hyphens, and underscores." + } + validation { + condition = length(var.cluster_config.name) <= 255 + error_message = "Cluster name must be 255 characters or less." + } + validation { + condition = alltrue([ + for subnet_id in var.cluster_config.subnet_ids : can(regex("^subnet-[0-9a-f]{8,17}$", subnet_id)) + ]) + error_message = "All subnet IDs must be valid AWS subnet IDs (format: subnet-xxxxxxxxx)." + } + validation { + condition = alltrue([ + for sg_id in var.cluster_config.security_group_ids : can(regex("^sg-[0-9a-f]{8,17}$", sg_id)) + ]) + error_message = "All security group IDs must be valid AWS security group IDs (format: sg-xxxxxxxxx)." + } +} + +variable "additional_tags" { + description = "Additional tags to apply to all resources" + type = map(string) + default = {} +} + +# Local values for consistent naming and tagging +locals { + # Consistent naming convention: {project_name}-{environment}-{resource_type} + name_prefix = "${var.project_name}-${var.environment}" + + # Common tags applied to all resources + common_tags = merge({ + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + Pattern = "EventBridge-ECS-Cron" + CreatedBy = "Terraform" + Owner = "DevOps" + CostCenter = var.project_name + }, var.additional_tags) +} + +# Variables for CloudWatch logging configuration +variable "log_retention_days" { + description = "CloudWatch log retention period in days" + type = number + default = 14 + validation { + condition = contains([ + 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653 + ], var.log_retention_days) + error_message = "Log retention days must be a valid CloudWatch retention period." + } +} + +variable "enable_log_insights" { + description = "Enable CloudWatch Logs Insights for structured logging" + type = bool + default = true +} + +variable "enable_monitoring" { + description = "Enable CloudWatch monitoring and alarms" + type = bool + default = true +} + + + + +variable "retry_policy" { + description = "EventBridge retry policy configuration" + type = object({ + maximum_retry_attempts = number + maximum_event_age_in_seconds = number + }) + default = { + maximum_retry_attempts = 3 + maximum_event_age_in_seconds = 3600 # 1 hour + } + validation { + condition = var.retry_policy.maximum_retry_attempts >= 0 && var.retry_policy.maximum_retry_attempts <= 185 + error_message = "Maximum retry attempts must be between 0 and 185." + } + validation { + condition = var.retry_policy.maximum_event_age_in_seconds >= 60 && var.retry_policy.maximum_event_age_in_seconds <= 86400 + error_message = "Maximum event age must be between 60 seconds (1 minute) and 86400 seconds (24 hours)." + } +} + +variable "ecs_task_retry_config" { + description = "ECS task failure handling configuration" + type = object({ + enable_circuit_breaker = bool + circuit_breaker_rollback = bool + deployment_maximum_percent = number + deployment_minimum_healthy_percent = number + enable_execute_command = bool + }) + default = { + enable_circuit_breaker = true + circuit_breaker_rollback = true + deployment_maximum_percent = 200 + deployment_minimum_healthy_percent = 100 + enable_execute_command = false + } + validation { + condition = var.ecs_task_retry_config.deployment_maximum_percent >= 100 && var.ecs_task_retry_config.deployment_maximum_percent <= 200 + error_message = "Deployment maximum percent must be between 100 and 200." + } + validation { + condition = var.ecs_task_retry_config.deployment_minimum_healthy_percent >= 0 && var.ecs_task_retry_config.deployment_minimum_healthy_percent <= 100 + error_message = "Deployment minimum healthy percent must be between 0 and 100." + } +} + + + +# CloudWatch Log Group for ECS tasks +resource "aws_cloudwatch_log_group" "ecs_logs" { + name = "/aws/ecs/${local.name_prefix}-task" + retention_in_days = var.log_retention_days + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-ecs-logs" + ResourceType = "CloudWatch-LogGroup" + LogType = "ECS-Task-Logs" + Purpose = "Scheduled-Task-Execution" + Component = "Logging" + }) +} + +# CloudWatch Log Group for EventBridge events +resource "aws_cloudwatch_log_group" "eventbridge_logs" { + name = "/aws/events/${local.name_prefix}-eventbridge" + retention_in_days = var.log_retention_days + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-eventbridge-logs" + ResourceType = "CloudWatch-LogGroup" + LogType = "EventBridge-Logs" + Purpose = "Schedule-Trigger-Events" + Component = "Logging" + }) +} + +# ECS Cluster +resource "aws_ecs_cluster" "main" { + name = "${local.name_prefix}-cluster" + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-cluster" + ResourceType = "ECS-Cluster" + Purpose = "Scheduled-Task-Execution" + Component = "Compute" + }) + + setting { + name = "containerInsights" + value = "enabled" + } + + configuration { + execute_command_configuration { + logging = "OVERRIDE" + log_configuration { + cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_logs.name + } + } + } +} + +# ECS Task Definition +resource "aws_ecs_task_definition" "main" { + family = "${local.name_prefix}-task" + network_mode = "awsvpc" + requires_compatibilities = [var.cluster_config.launch_type] + cpu = var.task_definition.cpu + memory = var.task_definition.memory + execution_role_arn = aws_iam_role.ecs_task_execution.arn + task_role_arn = aws_iam_role.ecs_task.arn + + container_definitions = jsonencode([ + { + name = "${local.name_prefix}-container" + image = var.task_definition.container_image + + essential = true + + # Simple command that demonstrates the cron job pattern using Amazon Linux tools + command = [ + "sh", "-c", + "echo \"{\\\"timestamp\\\":\\\"$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\\\",\\\"level\\\":\\\"INFO\\\",\\\"message\\\":\\\"EventBridge ECS Cron Job Started\\\",\\\"task_family\\\":\\\"${local.name_prefix}-task\\\",\\\"cluster_name\\\":\\\"${local.name_prefix}-cluster\\\"}\" && sleep 2 && echo \"{\\\"timestamp\\\":\\\"$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\\\",\\\"level\\\":\\\"INFO\\\",\\\"message\\\":\\\"Processing sample data\\\",\\\"records_processed\\\":42}\" && sleep 1 && echo \"{\\\"timestamp\\\":\\\"$(date -u +\"%Y-%m-%dT%H:%M:%SZ\")\\\",\\\"level\\\":\\\"INFO\\\",\\\"message\\\":\\\"EventBridge ECS Cron Job Completed Successfully\\\",\\\"duration_seconds\\\":3}\"" + ] + + environment = concat([ + for key, value in var.task_definition.environment_variables : { + name = key + value = value + } + ], [ + { + name = "LOG_LEVEL" + value = "INFO" + }, + { + name = "LOG_FORMAT" + value = "json" + }, + { + name = "TASK_FAMILY" + value = "${local.name_prefix}-task" + }, + { + name = "CLUSTER_NAME" + value = "${local.name_prefix}-cluster" + }, + { + name = "PROJECT_NAME" + value = var.project_name + }, + { + name = "ENVIRONMENT" + value = var.environment + } + ]) + + logConfiguration = { + logDriver = "awslogs" + options = merge({ + "awslogs-group" = aws_cloudwatch_log_group.ecs_logs.name + "awslogs-region" = data.aws_region.current.name + "awslogs-stream-prefix" = "ecs" + }, var.enable_log_insights ? { + "awslogs-create-group" = "true" + "mode" = "non-blocking" + "max-buffer-size" = "25m" + } : {}) + } + } + ]) + + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-task-definition" + ResourceType = "ECS-TaskDefinition" + Purpose = "Scheduled-Task-Execution" + Component = "Compute" + }) +} + +# Data source for current AWS region +data "aws_region" "current" {} + +# Data source for default VPC +data "aws_vpc" "default" { + default = true +} + +# Data source for default VPC subnets +data "aws_subnets" "default" { + filter { + name = "vpc-id" + values = [data.aws_vpc.default.id] + } +} + +# Security Group for ECS tasks +resource "aws_security_group" "ecs_tasks" { + name_prefix = "${local.name_prefix}-ecs-tasks" + description = "Security group for ECS tasks in ${local.name_prefix}" + vpc_id = data.aws_vpc.default.id + + # Allow outbound internet access for pulling container images and logging + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + description = "Allow all outbound traffic" + } + + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-ecs-tasks-sg" + ResourceType = "Security-Group" + Purpose = "ECS-Task-Networking" + Component = "Security" + }) +} + +# IAM Role for ECS Task Execution (used by ECS agent) +resource "aws_iam_role" "ecs_task_execution" { + name = "${local.name_prefix}-task-execution-role" + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-task-execution-role" + ResourceType = "IAM-Role" + Purpose = "ECS-Task-Execution" + Component = "Security" + }) + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} + +# IAM Policy for ECS Task Execution Role +resource "aws_iam_role_policy" "ecs_task_execution" { + name = "${local.name_prefix}-task-execution-policy" + role = aws_iam_role.ecs_task_execution.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "${aws_cloudwatch_log_group.ecs_logs.arn}:*" + } + ] + }) +} + +# IAM Role for ECS Task (used by application code) +resource "aws_iam_role" "ecs_task" { + name = "${local.name_prefix}-task-role" + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-task-role" + ResourceType = "IAM-Role" + Purpose = "ECS-Task-Application" + Component = "Security" + }) + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "ecs-tasks.amazonaws.com" + } + } + ] + }) +} + +# IAM Policy for ECS Task Role (minimal permissions for application) +resource "aws_iam_role_policy" "ecs_task" { + name = "${local.name_prefix}-task-policy" + role = aws_iam_role.ecs_task.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "${aws_cloudwatch_log_group.ecs_logs.arn}:*" + } + ] + }) +} + +# IAM Role for EventBridge to trigger ECS tasks +resource "aws_iam_role" "eventbridge_ecs" { + name = "${local.name_prefix}-eventbridge-role" + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-eventbridge-role" + ResourceType = "IAM-Role" + Purpose = "EventBridge-ECS-Trigger" + Component = "Security" + }) + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "events.amazonaws.com" + } + } + ] + }) +} + +# IAM Policy for EventBridge Role +resource "aws_iam_role_policy" "eventbridge_ecs" { + name = "${local.name_prefix}-eventbridge-policy" + role = aws_iam_role.eventbridge_ecs.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "ecs:RunTask" + ] + Resource = aws_ecs_task_definition.main.arn + }, + { + Effect = "Allow" + Action = [ + "iam:PassRole" + ] + Resource = [ + aws_iam_role.ecs_task_execution.arn, + aws_iam_role.ecs_task.arn + ] + } + ] + }) +} # EventBridge Rule for scheduled execution +resource "aws_cloudwatch_event_rule" "ecs_schedule" { + name = "${local.name_prefix}-schedule" + description = "Trigger ECS task on schedule for ${var.project_name} in ${var.environment}" + schedule_expression = var.schedule_expression + tags = merge(local.common_tags, { + Name = "${local.name_prefix}-schedule" + ResourceType = "EventBridge-Rule" + Purpose = "Scheduled-Task-Trigger" + Component = "Scheduling" + }) +} + +# EventBridge Target - ECS Task +resource "aws_cloudwatch_event_target" "ecs_target" { + rule = aws_cloudwatch_event_rule.ecs_schedule.name + target_id = "${local.name_prefix}-ecs-target" + arn = aws_ecs_cluster.main.arn + role_arn = aws_iam_role.eventbridge_ecs.arn + + ecs_target { + task_count = 1 + task_definition_arn = aws_ecs_task_definition.main.arn + launch_type = var.cluster_config.launch_type + platform_version = "LATEST" + + network_configuration { + subnets = length(var.cluster_config.subnet_ids) > 0 ? var.cluster_config.subnet_ids : data.aws_subnets.default.ids + security_groups = length(var.cluster_config.security_group_ids) > 0 ? var.cluster_config.security_group_ids : [aws_security_group.ecs_tasks.id] + assign_public_ip = var.cluster_config.assign_public_ip + } + } + + retry_policy { + maximum_retry_attempts = var.retry_policy.maximum_retry_attempts + maximum_event_age_in_seconds = var.retry_policy.maximum_event_age_in_seconds + } +} + + +# Outputs +output "eventbridge_rule_arn" { + description = "ARN of the EventBridge rule" + value = aws_cloudwatch_event_rule.ecs_schedule.arn +} + +output "eventbridge_rule_name" { + description = "Name of the EventBridge rule" + value = aws_cloudwatch_event_rule.ecs_schedule.name +} + +output "ecs_cluster_arn" { + description = "ARN of the ECS cluster" + value = aws_ecs_cluster.main.arn +} + +output "ecs_task_definition_arn" { + description = "ARN of the ECS task definition" + value = aws_ecs_task_definition.main.arn +} + +output "cloudwatch_log_group_name" { + description = "Name of the CloudWatch log group" + value = aws_cloudwatch_log_group.ecs_logs.name +} + +output "eventbridge_log_group_name" { + description = "Name of the EventBridge log group" + value = aws_cloudwatch_log_group.eventbridge_logs.name +} + + + diff --git a/eventbridge-scheduler-ecs-python-terraform/src/.env.example b/eventbridge-scheduler-ecs-python-terraform/src/.env.example new file mode 100644 index 000000000..381f3805d --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/src/.env.example @@ -0,0 +1,20 @@ +# Sample environment variables for the cron job application +# Copy this file to .env and modify values as needed + +# Database configuration +DATABASE_URL=postgresql://username:password@localhost:5432/database_name + +# External API configuration +API_KEY=your-api-key-here +EXTERNAL_API_URL=https://jsonplaceholder.typicode.com/posts + +# Processing configuration +BATCH_SIZE=100 +PROCESSING_DELAY=0.1 + +# Retry configuration +MAX_RETRIES=3 +RETRY_DELAY=1.0 + +# Logging configuration +LOG_LEVEL=INFO \ No newline at end of file diff --git a/eventbridge-scheduler-ecs-python-terraform/src/Dockerfile b/eventbridge-scheduler-ecs-python-terraform/src/Dockerfile new file mode 100644 index 000000000..40f7aa559 --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/src/Dockerfile @@ -0,0 +1,18 @@ +# Simple container for EventBridge ECS Cron Demo +FROM public.ecr.aws/amazonlinux/amazonlinux:2 + +# Set working directory +WORKDIR /app + +# Copy entrypoint script +COPY entrypoint.sh . + +# Make entrypoint executable +RUN chmod +x entrypoint.sh + +# Set default environment variables +ENV TASK_FAMILY="eventbridge-ecs-cron-task" +ENV CLUSTER_NAME="eventbridge-ecs-cron-cluster" + +# Run the entrypoint script +CMD ["./entrypoint.sh"] \ No newline at end of file diff --git a/eventbridge-scheduler-ecs-python-terraform/src/app.py b/eventbridge-scheduler-ecs-python-terraform/src/app.py new file mode 100644 index 000000000..59773a1f3 --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/src/app.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +""" +Simple Data Processing Job - EventBridge ECS Cron Pattern Demo +This job fetches data from an API, processes it, and logs the results. +Perfect for demonstrating scheduled batch processing with ECS. +""" + +import os +import json +import time +import requests +from datetime import datetime + + +def log_message(level, message, **extra): + """Simple structured logging for CloudWatch""" + log_entry = { + "timestamp": datetime.utcnow().isoformat() + "Z", + "level": level, + "message": message, + "task_family": os.getenv("TASK_FAMILY", "eventbridge-ecs-cron"), + "cluster_name": os.getenv("CLUSTER_NAME", "eventbridge-ecs-cron-cluster"), + **extra + } + print(json.dumps(log_entry)) + + +def fetch_sample_data(): + """Fetch sample data from a public API""" + try: + log_message("INFO", "Fetching sample data from API") + + # Use a free public API for demonstration + response = requests.get("https://jsonplaceholder.typicode.com/posts", timeout=10) + response.raise_for_status() + + data = response.json() + log_message("INFO", f"Successfully fetched {len(data)} posts from API") + return data + + except Exception as e: + log_message("ERROR", f"Failed to fetch data: {str(e)}") + raise + + +def process_data(posts): + """Process the fetched data - simple data transformation""" + log_message("INFO", f"Starting to process {len(posts)} posts") + + processed_data = [] + + for post in posts: + # Simple data processing: extract key information and add metadata + processed_post = { + "id": post["id"], + "user_id": post["userId"], + "title_length": len(post["title"]), + "body_length": len(post["body"]), + "word_count": len(post["body"].split()), + "processed_at": datetime.utcnow().isoformat() + "Z" + } + + processed_data.append(processed_post) + + # Add a small delay to simulate processing time + time.sleep(0.01) + + log_message("INFO", f"Successfully processed {len(processed_data)} posts") + return processed_data + + +def generate_report(processed_data): + """Generate a simple report from processed data""" + log_message("INFO", "Generating summary report") + + # Calculate some basic statistics + total_posts = len(processed_data) + avg_title_length = sum(p["title_length"] for p in processed_data) / total_posts + avg_word_count = sum(p["word_count"] for p in processed_data) / total_posts + + # Find the longest and shortest posts + longest_post = max(processed_data, key=lambda x: x["body_length"]) + shortest_post = min(processed_data, key=lambda x: x["body_length"]) + + report = { + "summary": { + "total_posts_processed": total_posts, + "average_title_length": round(avg_title_length, 2), + "average_word_count": round(avg_word_count, 2), + "longest_post_id": longest_post["id"], + "longest_post_length": longest_post["body_length"], + "shortest_post_id": shortest_post["id"], + "shortest_post_length": shortest_post["body_length"] + }, + "processing_completed_at": datetime.utcnow().isoformat() + "Z" + } + + log_message("INFO", "Report generated successfully", report=report) + return report + + +def save_results(report): + """Save results - in this demo, we just log them""" + log_message("INFO", "Saving processing results") + + # In a real scenario, you might save to S3, database, etc. + # For this demo, we'll just log the results + + # Simulate saving to different destinations + destinations = ["database", "s3_bucket", "cache"] + + for dest in destinations: + log_message("INFO", f"Saving results to {dest}") + time.sleep(0.1) # Simulate save time + + log_message("INFO", "Results saved successfully to all destinations") + + +def main(): + """Main job execution""" + start_time = datetime.utcnow() + + try: + log_message("INFO", "=== Starting Data Processing Job ===") + log_message("INFO", f"Job started at: {start_time.isoformat()}Z") + + # Step 1: Fetch data from external API + posts = fetch_sample_data() + + # Step 2: Process the data + processed_data = process_data(posts) + + # Step 3: Generate report + report = generate_report(processed_data) + + # Step 4: Save results + save_results(report) + + # Calculate execution time + end_time = datetime.utcnow() + duration = (end_time - start_time).total_seconds() + + log_message("INFO", "=== Job Completed Successfully ===", + duration_seconds=duration, + posts_processed=len(processed_data)) + + except Exception as e: + end_time = datetime.utcnow() + duration = (end_time - start_time).total_seconds() + + log_message("ERROR", f"Job failed: {str(e)}", + duration_seconds=duration, + error_type=type(e).__name__) + + # Exit with error code so ECS marks the task as failed + exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/eventbridge-scheduler-ecs-python-terraform/src/build.sh b/eventbridge-scheduler-ecs-python-terraform/src/build.sh new file mode 100755 index 000000000..4be6ee5eb --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/src/build.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Build script for the EventBridge ECS cron job container + +set -e + +# Configuration +IMAGE_NAME="eventbridge-ecs-cron" +IMAGE_TAG="${1:-latest}" +FULL_IMAGE_NAME="${IMAGE_NAME}:${IMAGE_TAG}" + +echo "Building Docker image: ${FULL_IMAGE_NAME}" + +# Build the Docker image +docker build -t "${FULL_IMAGE_NAME}" . + +echo "Build completed successfully!" +echo "Image: ${FULL_IMAGE_NAME}" + +# Show image details +echo "" +echo "Image details:" +docker images "${IMAGE_NAME}" --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}\t{{.CreatedAt}}" + +echo "" +echo "To run the container locally:" +echo "docker run --rm ${FULL_IMAGE_NAME}" +echo "" +echo "To run with environment variables:" +echo "docker run --rm --env-file .env ${FULL_IMAGE_NAME}" \ No newline at end of file diff --git a/eventbridge-scheduler-ecs-python-terraform/src/entrypoint.sh b/eventbridge-scheduler-ecs-python-terraform/src/entrypoint.sh new file mode 100755 index 000000000..b66db4d37 --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/src/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +# Simple entrypoint script for EventBridge ECS Cron Demo + +# Log start of job with timestamp +echo "{\"timestamp\":\"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",\"level\":\"INFO\",\"message\":\"EventBridge ECS Cron Job Started\",\"task_family\":\"$TASK_FAMILY\",\"cluster_name\":\"$CLUSTER_NAME\"}" + +# Simulate processing time +sleep 2 + +# Log processing status +echo "{\"timestamp\":\"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",\"level\":\"INFO\",\"message\":\"Processing sample data\",\"records_processed\":42}" + +# Simulate more processing +sleep 1 + +# Calculate execution time (simple approximation) +echo "{\"timestamp\":\"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",\"level\":\"INFO\",\"message\":\"EventBridge ECS Cron Job Completed Successfully\",\"duration_seconds\":3}" + +# Exit successfully +exit 0 \ No newline at end of file diff --git a/eventbridge-scheduler-ecs-python-terraform/src/requirements.txt b/eventbridge-scheduler-ecs-python-terraform/src/requirements.txt new file mode 100644 index 000000000..6bf5f6ce0 --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/src/requirements.txt @@ -0,0 +1,2 @@ +# Minimal dependencies for the sample cron job application +requests==2.31.0 \ No newline at end of file diff --git a/eventbridge-scheduler-ecs-python-terraform/src/test_local.py b/eventbridge-scheduler-ecs-python-terraform/src/test_local.py new file mode 100755 index 000000000..c75885926 --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/src/test_local.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +""" +Local test script for the EventBridge ECS Cron job +Run this to test the job locally before deploying to ECS +""" + +import os +import sys + +# Add the current directory to Python path so we can import app +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +# Set some test environment variables +os.environ["TASK_FAMILY"] = "local-test-task" +os.environ["CLUSTER_NAME"] = "local-test-cluster" + +# Import and run the main application +from app import main + +if __name__ == "__main__": + print("🚀 Running EventBridge ECS Cron Job locally...") + print("=" * 50) + + try: + main() + print("=" * 50) + print("✅ Job completed successfully!") + + except Exception as e: + print("=" * 50) + print(f"❌ Job failed: {e}") + sys.exit(1) \ No newline at end of file From 5b554b6f211a8de2e690ae43777af633f20210a8 Mon Sep 17 00:00:00 2001 From: Sudhansu Date: Wed, 23 Jul 2025 20:34:49 +0530 Subject: [PATCH 2/3] added pattern.json --- .../eventbridge-ecs-cron-pattern.json | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs-cron-pattern.json diff --git a/eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs-cron-pattern.json b/eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs-cron-pattern.json new file mode 100644 index 000000000..4a8bb95e9 --- /dev/null +++ b/eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs-cron-pattern.json @@ -0,0 +1,70 @@ +{ + "title": "Scheduled ECS Task Execution using EventBridge Cron and Terraform", + "description": "This pattern demonstrates how to use Amazon EventBridge to schedule and trigger Amazon ECS tasks on a cron schedule using Terraform. The pattern provides a serverless, cost-effective solution for running scheduled containerized workloads.", + "language": "Terraform", + "level": "200", + "framework": "Terraform", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern creates an automated scheduled task execution system using EventBridge cron rules with ECS Fargate tasks.", + "The EventBridge rule triggers on a configurable schedule (cron or rate expression) and launches an ECS task in the specified cluster.", + "The solution is fully configurable through Terraform variables and implements security best practices with least-privilege IAM roles." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-scheduler-ecs-python-terraform", + "templateURL": "serverless-patterns/eventbridge-ecs-cron-terraform", + "projectFolder": "eventbridge-ecs-cron-terraform", + "templateFile": "eventbridge-ecs.tf" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon EventBridge Scheduled Rules", + "link": "https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-create-rule-schedule.html" + }, + { + "text": "Amazon ECS Fargate", + "link": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/AWS_Fargate.html" + }, + { + "text": "EventBridge to ECS Integration", + "link": "https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html#eb-ecs-task-target" + }, + { + "text": "Terraform AWS Provider", + "link": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs" + } + ] + }, + "deploy": { + "text": [ + "terraform init", + "terraform plan", + "terraform apply" + ] + }, + "testing": { + "text": [ + "Check the EventBridge rule in the AWS Console to verify it's created with the correct schedule.", + "Manually trigger the rule to test the ECS task execution.", + "View CloudWatch logs to confirm the task is running as expected." + ] + }, + "cleanup": { + "text": [ + "terraform destroy", + "Confirm the destruction by typing 'yes' when prompted." + ] + }, + "authors": [ + { + "name": "Your Name", + "bio": "Your Role at AWS", + "linkedin": "https://www.linkedin.com/in/your-profile/" + } + ] +} \ No newline at end of file From 6cb99eb4ffeed98adcc0b42596a34e6e32821c8f Mon Sep 17 00:00:00 2001 From: Sudhansu Date: Wed, 23 Jul 2025 21:00:59 +0530 Subject: [PATCH 3/3] added missing details --- .../eventbridge-ecs-cron-pattern.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs-cron-pattern.json b/eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs-cron-pattern.json index 4a8bb95e9..1e1449de9 100644 --- a/eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs-cron-pattern.json +++ b/eventbridge-scheduler-ecs-python-terraform/eventbridge-ecs-cron-pattern.json @@ -62,9 +62,10 @@ }, "authors": [ { - "name": "Your Name", - "bio": "Your Role at AWS", - "linkedin": "https://www.linkedin.com/in/your-profile/" + "name": "Sudhansu Mohapatra", + "bio": "Assoc. Delivery Consultant at AWS", + "image": "https://drive.google.com/file/d/1uTQ3MbFvziRfBPWHsVsD5vbVyGuFO665/view?usp=sharing", + "linkedin": "https://www.linkedin.com/in/sudhansu-mohapatra-83a51811a/" } ] } \ No newline at end of file