[{"data":1,"prerenderedAt":1912},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob":3,"surround:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob":1879,"ancestors:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob":1888,"children:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob":1893,"related:\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob":1894},{"id":4,"title":5,"body":6,"dateModified":1845,"datePublished":1845,"description":1846,"extension":1847,"faq":1848,"keyword":1857,"kind":1858,"meta":1859,"navigation":960,"path":1868,"seo":1869,"slug":1872,"stem":1873,"topics":1874,"type":1877,"__hash__":1878},"docs\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob\u002Findex.md","Run an Excel Report as a Kubernetes CronJob",{"type":7,"value":8,"toc":1832},"minimark",[9,28,33,69,81,207,211,214,618,686,693,697,712,773,784,905,909,912,1104,1214,1220,1224,1231,1480,1551,1564,1568,1660,1679,1683,1697,1701,1744,1748,1767,1771,1777,1783,1789,1795,1799,1828],[10,11,12,13,17,18,21,22,27],"p",{},"Teams that already run their applications on Kubernetes often want their reports there too, rather than on a forgotten VM with a crontab. A ",[14,15,16],"em",{},"CronJob"," is Kubernetes' scheduler: on each tick it creates a ",[14,19,20],{},"Job",", which runs a pod with the report container to completion, retries it if it fails, and keeps a history of recent runs. The manifest has a handful of fields that decide whether it behaves well — whether a slow run can overlap the next one, what happens if Kubernetes was down at the scheduled time, how long a stuck run may live. This page, part of ",[23,24,26],"a",{"href":25},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002F","Scheduling Python Excel Scripts with Cron",", writes that manifest, checks it strictly against the Kubernetes schema before it goes near a cluster, and covers testing and troubleshooting.",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,40],"pre",{"className":36,"code":37,"language":38,"meta":39,"style":39},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install kubernetes-validate croniter\n# kubectl and access to a cluster to apply it\n","bash","",[41,42,43,62],"code",{"__ignoreMap":39},[44,45,48,52,56,59],"span",{"class":46,"line":47},"line",1,[44,49,51],{"class":50},"sMTad","pip",[44,53,55],{"class":54},"srMev"," install",[44,57,58],{"class":54}," kubernetes-validate",[44,60,61],{"class":54}," croniter\n",[44,63,65],{"class":46,"line":64},2,[44,66,68],{"class":67},"s-wDw","# kubectl and access to a cluster to apply it\n",[10,70,71,72,76,77,80],{},"The report is packaged as a container image as described in ",[23,73,75],{"href":74},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-python-excel-report-in-docker\u002F","Run a Python Excel Report in Docker"," and pushed to a registry the cluster can pull from. It reads its database password from the environment and uploads the finished workbook to object storage. The manifest below was checked with ",[41,78,79],{},"kubernetes-validate",", which validates against the official schemas for a given Kubernetes version without needing a cluster.",[82,83,91,92,91,96,91,100,91,108,91,118,91,129,91,135,91,140,91,144,91,149,91,155,91,160,91,167,91,171,91,175,91,179,91,182,91,186,91,191,91,198,91,203],"svg",{"viewBox":84,"role":85,"ariaLabelledBy":86,"xmlns":89,"style":90},"54 0 652 383","img",[87,88],"kc-layers-t","kc-layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:652px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[93,94,95],"title",{"id":87},"CronJob, Job, Pod",[97,98,99],"desc",{"id":88},"Three layers. The CronJob holds the schedule and creates a Job on each tick; the Job handles retries and the time limit and creates Pods; each Pod runs the report container once.",[101,102],"rect",{"x":103,"y":104,"width":105,"height":106,"fill":107},"54","0","652","383","#ffffff",[101,109],{"x":110,"y":111,"width":112,"height":113,"rx":114,"fill":115,"stroke":116,"style":117},"70.0","16.0","620.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[119,120,122],"g",{"transform":121},"translate(368.0,29.0) scale(1.000)",[123,124],"path",{"d":125,"fill":126,"stroke":127,"style":128},"M12 3a9 9 0 1 0 .01 0z M12 7v5l3.5 2","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[130,131,16],"text",{"x":132,"y":133,"style":134},"380.0","75.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle",[130,136,139],{"x":132,"y":137,"style":138},"95.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","schedule, timeZone, concurrencyPolicy, history",[46,141],{"x1":132,"y1":142,"x2":132,"y2":143,"stroke":116,"style":117},"114.0","136.0",[145,146],"polygon",{"points":147,"fill":148},"380.0,140.0 375.0,131.0 385.0,131.0","#5b5cf0",[130,150,154],{"x":151,"y":152,"style":153},"392.0","131.0","font-size:11px;font-weight:700;fill:var(--muted,#5b6780);text-anchor:start","creates on each tick",[101,156],{"x":110,"y":157,"width":112,"height":113,"rx":114,"fill":158,"stroke":159,"style":117},"145.0","#d9f4f1","var(--teal,#0f9488)",[119,161,163],{"transform":162},"translate(368.0,158.0) scale(1.000)",[123,164],{"d":165,"fill":126,"stroke":166,"style":128},"M20 11a8 8 0 1 0-2.3 5.7 M20 4v7h-7","var(--teal-ink,#0b6157)",[130,168,20],{"x":132,"y":169,"style":170},"204.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle",[130,172,174],{"x":132,"y":173,"style":138},"224.0","backoffLimit, activeDeadlineSeconds, ttlSecondsAfterFinished",[46,176],{"x1":132,"y1":177,"x2":132,"y2":178,"stroke":116,"style":117},"243.0","265.0",[145,180],{"points":181,"fill":148},"380.0,269.0 375.0,260.0 385.0,260.0",[130,183,185],{"x":151,"y":184,"style":153},"260.0","creates",[101,187],{"x":110,"y":188,"width":112,"height":113,"rx":114,"fill":189,"stroke":190,"style":117},"274.0","#e7ebef","var(--line,#cdd5e6)",[119,192,194],{"transform":193},"translate(368.0,287.0) scale(1.000)",[123,195],{"d":196,"fill":126,"stroke":197,"style":128},"M12 2.5l8.5 4.7v9.6L12 21.5l-8.5-4.7V7.2z M3.5 7.2l8.5 4.7 8.5-4.7 M12 11.9v9.6","var(--text,#172033)",[130,199,202],{"x":132,"y":200,"style":201},"333.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Pod",[130,204,206],{"x":132,"y":205,"style":138},"353.0","the report container, restartPolicy Never",[29,208,210],{"id":209},"step-1-the-manifest","Step 1 — The manifest",[10,212,213],{},"Every field below is there for a reason explained after it:",[34,215,219],{"className":216,"code":217,"language":218,"meta":39,"style":39},"language-yaml shiki shiki-themes github-light github-dark-high-contrast","apiVersion: batch\u002Fv1\nkind: CronJob\nmetadata:\n  name: weekly-sales-report\n  namespace: reporting\nspec:\n  schedule: \"0 7 * * 1\"\n  timeZone: \"Europe\u002FLondon\"\n  concurrencyPolicy: Forbid\n  startingDeadlineSeconds: 3600\n  successfulJobsHistoryLimit: 3\n  failedJobsHistoryLimit: 5\n  jobTemplate:\n    spec:\n      backoffLimit: 2\n      activeDeadlineSeconds: 1800\n      ttlSecondsAfterFinished: 604800\n      template:\n        spec:\n          restartPolicy: Never\n          containers:\n            - name: report\n              image: registry.example.com\u002Freports\u002Fweekly-sales:1.4.0\n              args: [\"--report-date\", \"auto\"]\n              env:\n                - name: DB_PASSWORD\n                  valueFrom:\n                    secretKeyRef:\n                      name: reporting-db\n                      key: password\n                - name: REPORT_BUCKET\n                  value: s3:\u002F\u002Facme-reports\u002Fsales\n              resources:\n                requests:\n                  cpu: 250m\n                  memory: 512Mi\n                limits:\n                  memory: 1Gi\n","yaml",[41,220,221,234,244,253,264,275,283,294,305,316,328,339,350,358,366,377,388,399,407,415,426,434,448,459,480,488,501,509,517,528,539,551,562,570,578,589,600,608],{"__ignoreMap":39},[44,222,223,227,231],{"class":46,"line":47},[44,224,226],{"class":225},"sx1sP","apiVersion",[44,228,230],{"class":229},"skGVy",": ",[44,232,233],{"class":54},"batch\u002Fv1\n",[44,235,236,239,241],{"class":46,"line":64},[44,237,238],{"class":225},"kind",[44,240,230],{"class":229},[44,242,243],{"class":54},"CronJob\n",[44,245,247,250],{"class":46,"line":246},3,[44,248,249],{"class":225},"metadata",[44,251,252],{"class":229},":\n",[44,254,256,259,261],{"class":46,"line":255},4,[44,257,258],{"class":225},"  name",[44,260,230],{"class":229},[44,262,263],{"class":54},"weekly-sales-report\n",[44,265,267,270,272],{"class":46,"line":266},5,[44,268,269],{"class":225},"  namespace",[44,271,230],{"class":229},[44,273,274],{"class":54},"reporting\n",[44,276,278,281],{"class":46,"line":277},6,[44,279,280],{"class":225},"spec",[44,282,252],{"class":229},[44,284,286,289,291],{"class":46,"line":285},7,[44,287,288],{"class":225},"  schedule",[44,290,230],{"class":229},[44,292,293],{"class":54},"\"0 7 * * 1\"\n",[44,295,297,300,302],{"class":46,"line":296},8,[44,298,299],{"class":225},"  timeZone",[44,301,230],{"class":229},[44,303,304],{"class":54},"\"Europe\u002FLondon\"\n",[44,306,308,311,313],{"class":46,"line":307},9,[44,309,310],{"class":225},"  concurrencyPolicy",[44,312,230],{"class":229},[44,314,315],{"class":54},"Forbid\n",[44,317,319,322,324],{"class":46,"line":318},10,[44,320,321],{"class":225},"  startingDeadlineSeconds",[44,323,230],{"class":229},[44,325,327],{"class":326},"sP0c6","3600\n",[44,329,331,334,336],{"class":46,"line":330},11,[44,332,333],{"class":225},"  successfulJobsHistoryLimit",[44,335,230],{"class":229},[44,337,338],{"class":326},"3\n",[44,340,342,345,347],{"class":46,"line":341},12,[44,343,344],{"class":225},"  failedJobsHistoryLimit",[44,346,230],{"class":229},[44,348,349],{"class":326},"5\n",[44,351,353,356],{"class":46,"line":352},13,[44,354,355],{"class":225},"  jobTemplate",[44,357,252],{"class":229},[44,359,361,364],{"class":46,"line":360},14,[44,362,363],{"class":225},"    spec",[44,365,252],{"class":229},[44,367,369,372,374],{"class":46,"line":368},15,[44,370,371],{"class":225},"      backoffLimit",[44,373,230],{"class":229},[44,375,376],{"class":326},"2\n",[44,378,380,383,385],{"class":46,"line":379},16,[44,381,382],{"class":225},"      activeDeadlineSeconds",[44,384,230],{"class":229},[44,386,387],{"class":326},"1800\n",[44,389,391,394,396],{"class":46,"line":390},17,[44,392,393],{"class":225},"      ttlSecondsAfterFinished",[44,395,230],{"class":229},[44,397,398],{"class":326},"604800\n",[44,400,402,405],{"class":46,"line":401},18,[44,403,404],{"class":225},"      template",[44,406,252],{"class":229},[44,408,410,413],{"class":46,"line":409},19,[44,411,412],{"class":225},"        spec",[44,414,252],{"class":229},[44,416,418,421,423],{"class":46,"line":417},20,[44,419,420],{"class":225},"          restartPolicy",[44,422,230],{"class":229},[44,424,425],{"class":54},"Never\n",[44,427,429,432],{"class":46,"line":428},21,[44,430,431],{"class":225},"          containers",[44,433,252],{"class":229},[44,435,437,440,443,445],{"class":46,"line":436},22,[44,438,439],{"class":229},"            - ",[44,441,442],{"class":225},"name",[44,444,230],{"class":229},[44,446,447],{"class":54},"report\n",[44,449,451,454,456],{"class":46,"line":450},23,[44,452,453],{"class":225},"              image",[44,455,230],{"class":229},[44,457,458],{"class":54},"registry.example.com\u002Freports\u002Fweekly-sales:1.4.0\n",[44,460,462,465,468,471,474,477],{"class":46,"line":461},24,[44,463,464],{"class":225},"              args",[44,466,467],{"class":229},": [",[44,469,470],{"class":54},"\"--report-date\"",[44,472,473],{"class":229},", ",[44,475,476],{"class":54},"\"auto\"",[44,478,479],{"class":229},"]\n",[44,481,483,486],{"class":46,"line":482},25,[44,484,485],{"class":225},"              env",[44,487,252],{"class":229},[44,489,491,494,496,498],{"class":46,"line":490},26,[44,492,493],{"class":229},"                - ",[44,495,442],{"class":225},[44,497,230],{"class":229},[44,499,500],{"class":54},"DB_PASSWORD\n",[44,502,504,507],{"class":46,"line":503},27,[44,505,506],{"class":225},"                  valueFrom",[44,508,252],{"class":229},[44,510,512,515],{"class":46,"line":511},28,[44,513,514],{"class":225},"                    secretKeyRef",[44,516,252],{"class":229},[44,518,520,523,525],{"class":46,"line":519},29,[44,521,522],{"class":225},"                      name",[44,524,230],{"class":229},[44,526,527],{"class":54},"reporting-db\n",[44,529,531,534,536],{"class":46,"line":530},30,[44,532,533],{"class":225},"                      key",[44,535,230],{"class":229},[44,537,538],{"class":54},"password\n",[44,540,542,544,546,548],{"class":46,"line":541},31,[44,543,493],{"class":229},[44,545,442],{"class":225},[44,547,230],{"class":229},[44,549,550],{"class":54},"REPORT_BUCKET\n",[44,552,554,557,559],{"class":46,"line":553},32,[44,555,556],{"class":225},"                  value",[44,558,230],{"class":229},[44,560,561],{"class":54},"s3:\u002F\u002Facme-reports\u002Fsales\n",[44,563,565,568],{"class":46,"line":564},33,[44,566,567],{"class":225},"              resources",[44,569,252],{"class":229},[44,571,573,576],{"class":46,"line":572},34,[44,574,575],{"class":225},"                requests",[44,577,252],{"class":229},[44,579,581,584,586],{"class":46,"line":580},35,[44,582,583],{"class":225},"                  cpu",[44,585,230],{"class":229},[44,587,588],{"class":54},"250m\n",[44,590,592,595,597],{"class":46,"line":591},36,[44,593,594],{"class":225},"                  memory",[44,596,230],{"class":229},[44,598,599],{"class":54},"512Mi\n",[44,601,603,606],{"class":46,"line":602},37,[44,604,605],{"class":225},"                limits",[44,607,252],{"class":229},[44,609,611,613,615],{"class":46,"line":610},38,[44,612,594],{"class":225},[44,614,230],{"class":229},[44,616,617],{"class":54},"1Gi\n",[619,620,621,638,646,654,662,670,678],"ul",{},[622,623,624,634,635,637],"li",{},[625,626,627,630,631],"strong",{},[41,628,629],{},"schedule"," and ",[41,632,633],{},"timeZone"," — standard five-field cron, evaluated in the named zone, so 07:00 means London time through the clock changes. Without ",[41,636,633],{}," it is the controller's zone, usually UTC.",[622,639,640,645],{},[625,641,642],{},[41,643,644],{},"concurrencyPolicy: Forbid"," — if last week's run is somehow still going, this week's is skipped rather than started alongside it. Two copies writing the same workbook is how reports get corrupted.",[622,647,648,653],{},[625,649,650],{},[41,651,652],{},"startingDeadlineSeconds"," — if the cluster was down or overloaded at 07:00, the job may still start up to an hour late; after that the run is skipped and counted as missed.",[622,655,656,661],{},[625,657,658],{},[41,659,660],{},"backoffLimit"," — the Job retries a failed pod twice, with increasing delays, before giving up.",[622,663,664,669],{},[625,665,666],{},[41,667,668],{},"activeDeadlineSeconds"," — a hard limit of 30 minutes for the whole Job, retries included; a hung database query cannot keep a pod alive for days.",[622,671,672,677],{},[625,673,674],{},[41,675,676],{},"ttlSecondsAfterFinished"," — finished Jobs and their pods are deleted after a week, so logs are available for review but do not pile up.",[622,679,680,685],{},[625,681,682],{},[41,683,684],{},"restartPolicy: Never"," — a failed container is not restarted in place; the Job creates a new pod, and each attempt keeps its own logs.",[10,687,688,689,692],{},"The image has an explicit version tag rather than ",[41,690,691],{},"latest",", so a new build does not change what Monday's run executes without a deliberate update.",[29,694,696],{"id":695},"step-2-validate-strictly-before-applying","Step 2 — Validate strictly before applying",[10,698,699,700,703,704,707,708,711],{},"A misspelled field is the most common CronJob bug, and a cluster does not necessarily refuse it: by default, recent versions of ",[41,701,702],{},"kubectl"," print a warning about an unknown field and apply the rest, so ",[41,705,706],{},"concurencyPolicy"," with one ",[41,709,710],{},"r"," is dropped and the job runs with the default policy that allows overlaps. A warning in the middle of a deployment log is easy to miss. Validate the manifest against the schema in strict mode, which rejects unknown fields:",[34,713,715],{"className":36,"code":714,"language":38,"meta":39,"style":39},"kubernetes-validate --strict -k 1.33.0 cronjob.yaml\nsed 's\u002FconcurrencyPolicy\u002FconcurencyPolicy\u002F' cronjob.yaml > cronjob_typo.yaml\nkubernetes-validate --strict -k 1.33.0 cronjob_typo.yaml || echo \"rejected, as it should be\"\n",[41,716,717,733,751],{"__ignoreMap":39},[44,718,719,721,724,727,730],{"class":46,"line":47},[44,720,79],{"class":50},[44,722,723],{"class":326}," --strict",[44,725,726],{"class":326}," -k",[44,728,729],{"class":326}," 1.33.0",[44,731,732],{"class":54}," cronjob.yaml\n",[44,734,735,738,741,744,748],{"class":46,"line":64},[44,736,737],{"class":50},"sed",[44,739,740],{"class":54}," 's\u002FconcurrencyPolicy\u002FconcurencyPolicy\u002F'",[44,742,743],{"class":54}," cronjob.yaml",[44,745,747],{"class":746},"s-kum"," >",[44,749,750],{"class":54}," cronjob_typo.yaml\n",[44,752,753,755,757,759,761,764,767,770],{"class":46,"line":246},[44,754,79],{"class":50},[44,756,723],{"class":326},[44,758,726],{"class":326},[44,760,729],{"class":326},[44,762,763],{"class":54}," cronjob_typo.yaml",[44,765,766],{"class":746}," ||",[44,768,769],{"class":326}," echo",[44,771,772],{"class":54}," \"rejected, as it should be\"\n",[10,774,775,776,779,780,783],{},"The real manifest passes; the one with the typo is rejected with the offending field named. Running this in CI, next to the report's tests, catches mistakes before ",[41,777,778],{},"kubectl apply"," does — or, worse, before the first silent misbehaviour does. On a cluster, ",[41,781,782],{},"kubectl apply --dry-run=server -f cronjob.yaml"," adds a check by the API server itself, including admission policies.",[82,785,91,791,91,794,91,797,91,802,91,810,91,818,91,824,91,830,91,836,91,844,91,848,91,853,91,857,91,862,91,865,91,871,91,876,91,880,91,884,91,889,91,892,91,897,91,900],{"viewBox":786,"role":85,"ariaLabelledBy":787,"xmlns":89,"style":790},"4 0 752 207",[788,789],"kc-typo-t","kc-typo-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[93,792,793],{"id":788},"What strict validation catches",[97,795,796],{"id":789},"Two panels. Without strict validation a misspelled field such as concurencyPolicy is accepted and silently ignored, so overlapping runs are allowed. With strict validation the unknown field is rejected before the manifest is applied.",[101,798],{"x":799,"y":104,"width":800,"height":801,"fill":107},"4","752","207",[101,803],{"x":804,"y":111,"width":805,"height":806,"rx":807,"fill":808,"stroke":809,"style":117},"20.0","351.0","175.0","14","#fdefd8","var(--gold,#b4740a)",[119,811,813],{"transform":812},"translate(182.5,29.0) scale(1.083)",[123,814],{"d":815,"fill":126,"stroke":816,"style":817},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[130,819,823],{"x":820,"y":821,"style":822},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Applied as is",[46,825],{"x1":826,"y1":827,"x2":828,"y2":827,"stroke":809,"style":829},"36.0","91.0","355.0","stroke-width:1px",[130,831,835],{"x":832,"y":833,"style":834},"60.0","111.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","unknown field ignored",[119,837,839],{"transform":838},"translate(38.5,99.5) scale(0.625)",[123,840],{"d":841,"fill":126,"stroke":842,"style":843},"M6 6l12 12 M18 6L6 18","var(--accent,#f43f8f)","stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[130,845,847],{"x":832,"y":846,"style":834},"137.0","default policy: overlaps allowed",[119,849,851],{"transform":850},"translate(38.5,125.5) scale(0.625)",[123,852],{"d":841,"fill":126,"stroke":842,"style":843},[130,854,856],{"x":832,"y":855,"style":834},"163.0","found only when runs collide",[119,858,860],{"transform":859},"translate(38.5,151.5) scale(0.625)",[123,861],{"d":841,"fill":126,"stroke":842,"style":843},[101,863],{"x":864,"y":111,"width":805,"height":806,"rx":807,"fill":158,"stroke":159,"style":117},"389.0",[119,866,868],{"transform":867},"translate(551.5,29.0) scale(1.083)",[123,869],{"d":870,"fill":126,"stroke":166,"style":817},"M5 12.5l4.5 4.5L19 7",[130,872,875],{"x":873,"y":821,"style":874},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Validated strictly",[46,877],{"x1":878,"y1":827,"x2":879,"y2":827,"stroke":159,"style":829},"405.0","724.0",[130,881,883],{"x":882,"y":833,"style":834},"429.0","unknown field rejected",[119,885,887],{"transform":886},"translate(407.5,99.5) scale(0.625)",[123,888],{"d":870,"fill":126,"stroke":159,"style":843},[130,890,891],{"x":882,"y":846,"style":834},"field name in the error",[119,893,895],{"transform":894},"translate(407.5,125.5) scale(0.625)",[123,896],{"d":870,"fill":126,"stroke":159,"style":843},[130,898,899],{"x":882,"y":855,"style":834},"fails in CI, not in production",[119,901,903],{"transform":902},"translate(407.5,151.5) scale(0.625)",[123,904],{"d":870,"fill":126,"stroke":159,"style":843},[29,906,908],{"id":907},"step-3-check-the-schedule-in-its-time-zone","Step 3 — Check the schedule in its time zone",[10,910,911],{},"Preview the next runs in the configured zone, including across the October clock change, to confirm the expression and the zone say what you mean:",[34,913,917],{"className":914,"code":915,"language":916,"meta":39,"style":39},"language-python shiki shiki-themes github-light github-dark-high-contrast","from datetime import datetime\nfrom zoneinfo import ZoneInfo\nfrom croniter import croniter\n\ntz = ZoneInfo(\"Europe\u002FLondon\")\nit = croniter(\"0 7 * * 1\", datetime(2026, 10, 12, 12, 0, tzinfo=tz))\nfor _ in range(3):\n    run = it.get_next(datetime)\n    print(run.strftime(\"%a %d %b %H:%M %Z\"), \"=\", run.astimezone(ZoneInfo(\"UTC\")).strftime(\"%H:%M UTC\"))\n","python",[41,918,919,933,945,956,962,979,1026,1049,1059],{"__ignoreMap":39},[44,920,921,924,927,930],{"class":46,"line":47},[44,922,923],{"class":746},"from",[44,925,926],{"class":229}," datetime ",[44,928,929],{"class":746},"import",[44,931,932],{"class":229}," datetime\n",[44,934,935,937,940,942],{"class":46,"line":64},[44,936,923],{"class":746},[44,938,939],{"class":229}," zoneinfo ",[44,941,929],{"class":746},[44,943,944],{"class":229}," ZoneInfo\n",[44,946,947,949,952,954],{"class":46,"line":246},[44,948,923],{"class":746},[44,950,951],{"class":229}," croniter ",[44,953,929],{"class":746},[44,955,61],{"class":229},[44,957,958],{"class":46,"line":255},[44,959,961],{"emptyLinePlaceholder":960},true,"\n",[44,963,964,967,970,973,976],{"class":46,"line":266},[44,965,966],{"class":229},"tz ",[44,968,969],{"class":746},"=",[44,971,972],{"class":229}," ZoneInfo(",[44,974,975],{"class":54},"\"Europe\u002FLondon\"",[44,977,978],{"class":229},")\n",[44,980,981,984,986,989,992,995,998,1000,1003,1005,1007,1009,1011,1013,1015,1017,1021,1023],{"class":46,"line":277},[44,982,983],{"class":229},"it ",[44,985,969],{"class":746},[44,987,988],{"class":229}," croniter(",[44,990,991],{"class":54},"\"0 7 * * 1\"",[44,993,994],{"class":229},", datetime(",[44,996,997],{"class":326},"2026",[44,999,473],{"class":229},[44,1001,1002],{"class":326},"10",[44,1004,473],{"class":229},[44,1006,114],{"class":326},[44,1008,473],{"class":229},[44,1010,114],{"class":326},[44,1012,473],{"class":229},[44,1014,104],{"class":326},[44,1016,473],{"class":229},[44,1018,1020],{"class":1019},"sa561","tzinfo",[44,1022,969],{"class":746},[44,1024,1025],{"class":229},"tz))\n",[44,1027,1028,1031,1034,1037,1040,1043,1046],{"class":46,"line":285},[44,1029,1030],{"class":746},"for",[44,1032,1033],{"class":229}," _ ",[44,1035,1036],{"class":746},"in",[44,1038,1039],{"class":326}," range",[44,1041,1042],{"class":229},"(",[44,1044,1045],{"class":326},"3",[44,1047,1048],{"class":229},"):\n",[44,1050,1051,1054,1056],{"class":46,"line":296},[44,1052,1053],{"class":229},"    run ",[44,1055,969],{"class":746},[44,1057,1058],{"class":229}," it.get_next(datetime)\n",[44,1060,1061,1064,1067,1070,1074,1077,1080,1083,1086,1089,1092,1095,1098,1101],{"class":46,"line":307},[44,1062,1063],{"class":326},"    print",[44,1065,1066],{"class":229},"(run.strftime(",[44,1068,1069],{"class":54},"\"",[44,1071,1073],{"class":1072},"sSjpA","%a",[44,1075,1076],{"class":1072}," %d",[44,1078,1079],{"class":1072}," %b",[44,1081,1082],{"class":54}," %H:%M %Z\"",[44,1084,1085],{"class":229},"), ",[44,1087,1088],{"class":54},"\"=\"",[44,1090,1091],{"class":229},", run.astimezone(ZoneInfo(",[44,1093,1094],{"class":54},"\"UTC\"",[44,1096,1097],{"class":229},")).strftime(",[44,1099,1100],{"class":54},"\"%H:%M UTC\"",[44,1102,1103],{"class":229},"))\n",[82,1105,91,1110,91,1113,91,1116,91,1119,91,1126,91,1130,91,1135,91,1141,91,1145,91,1149,91,1153,91,1157,91,1162,91,1168,91,1172,91,1174,91,1176,91,1179,91,1184,91,1189,91,1192,91,1194,91,1196,91,1199,91,1204,91,1208,91,1210,91,1212],{"viewBox":1106,"role":85,"ariaLabelledBy":1107,"xmlns":89,"style":790},"4 0 752 279",[1108,1109],"kc-dst-t","kc-dst-d",[93,1111,1112],{"id":1108},"The same schedule across the October clock change",[97,1114,1115],{"id":1109},"A timeline of three Monday runs. On 19 October the 07:00 London run is 06:00 UTC; after the clocks go back on 25 October, the 26 October and 2 November runs are 07:00 UTC, still 07:00 in London.",[101,1117],{"x":799,"y":104,"width":800,"height":1118,"fill":107},"279",[46,1120],{"x1":1121,"y1":1122,"x2":1123,"y2":1122,"stroke":1124,"style":1125},"94.0","139.5","666.0","var(--muted,#5b6780)","stroke-width:2.5px",[101,1127],{"x":804,"y":111,"width":1128,"height":1129,"rx":114,"fill":115,"stroke":116,"style":117},"172.0","91.5",[119,1131,1133],{"transform":1132},"translate(94.0,29.0) scale(1.000)",[123,1134],{"d":125,"fill":126,"stroke":127,"style":128},[130,1136,1140],{"x":1137,"y":1138,"style":1139},"106.0","74.5","font-size:12.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Mon 19 Oct",[130,1142,1144],{"x":1137,"y":113,"style":1143},"font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","07:00 BST = 06:00 UTC",[46,1146],{"x1":1137,"y1":1147,"x2":1137,"y2":1122,"stroke":1124,"style":1148},"107.5","stroke-width:1.5px",[1150,1151],"circle",{"cx":1137,"cy":1122,"r":1152,"fill":116,"stroke":107,"style":117},"7.0",[101,1154],{"x":1155,"y":1156,"width":1128,"height":1129,"rx":114,"fill":189,"stroke":190,"style":117},"202.7","171.5",[119,1158,1160],{"transform":1159},"translate(276.7,184.5) scale(1.000)",[123,1161],{"d":165,"fill":126,"stroke":197,"style":128},[130,1163,1167],{"x":1164,"y":1165,"style":1166},"288.7","230.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Sun 25 Oct",[130,1169,1171],{"x":1164,"y":1170,"style":1143},"248.5","clocks go back",[46,1173],{"x1":1164,"y1":1122,"x2":1164,"y2":1156,"stroke":1124,"style":1148},[1150,1175],{"cx":1164,"cy":1122,"r":1152,"fill":1124,"stroke":107,"style":117},[101,1177],{"x":1178,"y":111,"width":1128,"height":1129,"rx":114,"fill":158,"stroke":159,"style":117},"385.3",[119,1180,1182],{"transform":1181},"translate(459.3,29.0) scale(1.000)",[123,1183],{"d":125,"fill":126,"stroke":166,"style":128},[130,1185,1188],{"x":1186,"y":1138,"style":1187},"471.3","font-size:12.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Mon 26 Oct",[130,1190,1191],{"x":1186,"y":113,"style":1143},"07:00 GMT = 07:00 UTC",[46,1193],{"x1":1186,"y1":1147,"x2":1186,"y2":1122,"stroke":1124,"style":1148},[1150,1195],{"cx":1186,"cy":1122,"r":1152,"fill":159,"stroke":107,"style":117},[101,1197],{"x":1198,"y":1156,"width":1128,"height":1129,"rx":114,"fill":158,"stroke":159,"style":117},"568.0",[119,1200,1202],{"transform":1201},"translate(642.0,184.5) scale(1.000)",[123,1203],{"d":125,"fill":126,"stroke":166,"style":128},[130,1205,1207],{"x":1206,"y":1165,"style":1187},"654.0","Mon 02 Nov",[130,1209,1191],{"x":1206,"y":1170,"style":1143},[46,1211],{"x1":1206,"y1":1122,"x2":1206,"y2":1156,"stroke":1124,"style":1148},[1150,1213],{"cx":1206,"cy":1122,"r":1152,"fill":159,"stroke":107,"style":117},[10,1215,1216,1217,1219],{},"The run stays at 07:00 local time while its UTC time moves from 06:00 to 07:00 after the clocks go back on 25 October — which is exactly what ",[41,1218,633],{}," buys. A schedule written in UTC without it would drift by an hour relative to the office twice a year.",[29,1221,1223],{"id":1222},"step-4-make-the-script-a-good-pod-citizen","Step 4 — Make the script a good pod citizen",[10,1225,1226,1227,1230],{},"Kubernetes judges each attempt by the container's exit code and stops pods with ",[41,1228,1229],{},"SIGTERM"," when the deadline passes or the node is drained. The script should exit non-zero on failure and stop cleanly when asked:",[34,1232,1234],{"className":914,"code":1233,"language":916,"meta":39,"style":39},"import signal\nimport sys\nimport time\n\nstopping = False\n\ndef handle_sigterm(signum, frame):\n    global stopping\n    stopping = True\n    print(\"SIGTERM received, stopping after the current step\", flush=True)\n\nsignal.signal(signal.SIGTERM, handle_sigterm)\n\ndef main() -> int:\n    for step in (\"extract\", \"build\", \"upload\"):\n        if stopping:\n            return 1\n        print(f\"{step} …\", flush=True)\n        time.sleep(0.2)\n    return 0\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n",[41,1235,1236,1243,1250,1257,1261,1271,1275,1287,1295,1305,1326,1330,1340,1344,1359,1387,1395,1403,1437,1447,1455,1459,1475],{"__ignoreMap":39},[44,1237,1238,1240],{"class":46,"line":47},[44,1239,929],{"class":746},[44,1241,1242],{"class":229}," signal\n",[44,1244,1245,1247],{"class":46,"line":64},[44,1246,929],{"class":746},[44,1248,1249],{"class":229}," sys\n",[44,1251,1252,1254],{"class":46,"line":246},[44,1253,929],{"class":746},[44,1255,1256],{"class":229}," time\n",[44,1258,1259],{"class":46,"line":255},[44,1260,961],{"emptyLinePlaceholder":960},[44,1262,1263,1266,1268],{"class":46,"line":266},[44,1264,1265],{"class":229},"stopping ",[44,1267,969],{"class":746},[44,1269,1270],{"class":326}," False\n",[44,1272,1273],{"class":46,"line":277},[44,1274,961],{"emptyLinePlaceholder":960},[44,1276,1277,1280,1284],{"class":46,"line":285},[44,1278,1279],{"class":746},"def",[44,1281,1283],{"class":1282},"s_Opv"," handle_sigterm",[44,1285,1286],{"class":229},"(signum, frame):\n",[44,1288,1289,1292],{"class":46,"line":296},[44,1290,1291],{"class":746},"    global",[44,1293,1294],{"class":229}," stopping\n",[44,1296,1297,1300,1302],{"class":46,"line":307},[44,1298,1299],{"class":229},"    stopping ",[44,1301,969],{"class":746},[44,1303,1304],{"class":326}," True\n",[44,1306,1307,1309,1311,1314,1316,1319,1321,1324],{"class":46,"line":318},[44,1308,1063],{"class":326},[44,1310,1042],{"class":229},[44,1312,1313],{"class":54},"\"SIGTERM received, stopping after the current step\"",[44,1315,473],{"class":229},[44,1317,1318],{"class":1019},"flush",[44,1320,969],{"class":746},[44,1322,1323],{"class":326},"True",[44,1325,978],{"class":229},[44,1327,1328],{"class":46,"line":330},[44,1329,961],{"emptyLinePlaceholder":960},[44,1331,1332,1335,1337],{"class":46,"line":341},[44,1333,1334],{"class":229},"signal.signal(signal.",[44,1336,1229],{"class":326},[44,1338,1339],{"class":229},", handle_sigterm)\n",[44,1341,1342],{"class":46,"line":352},[44,1343,961],{"emptyLinePlaceholder":960},[44,1345,1346,1348,1351,1354,1357],{"class":46,"line":360},[44,1347,1279],{"class":746},[44,1349,1350],{"class":1282}," main",[44,1352,1353],{"class":229},"() -> ",[44,1355,1356],{"class":326},"int",[44,1358,252],{"class":229},[44,1360,1361,1364,1367,1369,1372,1375,1377,1380,1382,1385],{"class":46,"line":368},[44,1362,1363],{"class":746},"    for",[44,1365,1366],{"class":229}," step ",[44,1368,1036],{"class":746},[44,1370,1371],{"class":229}," (",[44,1373,1374],{"class":54},"\"extract\"",[44,1376,473],{"class":229},[44,1378,1379],{"class":54},"\"build\"",[44,1381,473],{"class":229},[44,1383,1384],{"class":54},"\"upload\"",[44,1386,1048],{"class":229},[44,1388,1389,1392],{"class":46,"line":379},[44,1390,1391],{"class":746},"        if",[44,1393,1394],{"class":229}," stopping:\n",[44,1396,1397,1400],{"class":46,"line":390},[44,1398,1399],{"class":746},"            return",[44,1401,1402],{"class":326}," 1\n",[44,1404,1405,1408,1410,1413,1415,1418,1421,1424,1427,1429,1431,1433,1435],{"class":46,"line":401},[44,1406,1407],{"class":326},"        print",[44,1409,1042],{"class":229},[44,1411,1412],{"class":746},"f",[44,1414,1069],{"class":54},[44,1416,1417],{"class":1072},"{",[44,1419,1420],{"class":229},"step",[44,1422,1423],{"class":1072},"}",[44,1425,1426],{"class":54}," …\"",[44,1428,473],{"class":229},[44,1430,1318],{"class":1019},[44,1432,969],{"class":746},[44,1434,1323],{"class":326},[44,1436,978],{"class":229},[44,1438,1439,1442,1445],{"class":46,"line":409},[44,1440,1441],{"class":229},"        time.sleep(",[44,1443,1444],{"class":326},"0.2",[44,1446,978],{"class":229},[44,1448,1449,1452],{"class":46,"line":417},[44,1450,1451],{"class":746},"    return",[44,1453,1454],{"class":326}," 0\n",[44,1456,1457],{"class":46,"line":428},[44,1458,961],{"emptyLinePlaceholder":960},[44,1460,1461,1464,1467,1470,1473],{"class":46,"line":436},[44,1462,1463],{"class":746},"if",[44,1465,1466],{"class":326}," __name__",[44,1468,1469],{"class":746}," ==",[44,1471,1472],{"class":54}," \"__main__\"",[44,1474,252],{"class":229},[44,1476,1477],{"class":46,"line":450},[44,1478,1479],{"class":229},"    sys.exit(main())\n",[34,1481,1483],{"className":36,"code":1482,"language":38,"meta":39,"style":39},"python report.py; echo \"exit $?\"\npython report.py & sleep 0.3; kill -TERM $!; wait $!; echo \"exit after SIGTERM: $?\"\n",[41,1484,1485,1507],{"__ignoreMap":39},[44,1486,1487,1489,1492,1495,1498,1501,1504],{"class":46,"line":47},[44,1488,916],{"class":50},[44,1490,1491],{"class":54}," report.py",[44,1493,1494],{"class":229},"; ",[44,1496,1497],{"class":326},"echo",[44,1499,1500],{"class":54}," \"exit ",[44,1502,1503],{"class":326},"$?",[44,1505,1506],{"class":54},"\"\n",[44,1508,1509,1511,1513,1516,1519,1522,1524,1527,1530,1533,1535,1538,1540,1542,1544,1547,1549],{"class":46,"line":64},[44,1510,916],{"class":50},[44,1512,1491],{"class":54},[44,1514,1515],{"class":229}," & ",[44,1517,1518],{"class":50},"sleep",[44,1520,1521],{"class":326}," 0.3",[44,1523,1494],{"class":229},[44,1525,1526],{"class":326},"kill",[44,1528,1529],{"class":326}," -TERM",[44,1531,1532],{"class":326}," $!",[44,1534,1494],{"class":229},[44,1536,1537],{"class":326},"wait",[44,1539,1532],{"class":326},[44,1541,1494],{"class":229},[44,1543,1497],{"class":326},[44,1545,1546],{"class":54}," \"exit after SIGTERM: ",[44,1548,1503],{"class":326},[44,1550,1506],{"class":54},[10,1552,1553,1554,1556,1557,1560,1561,1563],{},"Run normally, it completes all three steps and exits 0. Sent ",[41,1555,1229],{}," part-way through, it finishes the current step, skips the rest and exits 1, so the Job records a failure instead of a success for a report that was never uploaded. Kubernetes waits ",[41,1558,1559],{},"terminationGracePeriodSeconds"," — 30 seconds by default — after ",[41,1562,1229],{}," before killing the container.",[29,1565,1567],{"id":1566},"step-5-apply-test-and-troubleshoot","Step 5 — Apply, test and troubleshoot",[34,1569,1571],{"className":36,"code":1570,"language":38,"meta":39,"style":39},"kubectl apply -f cronjob.yaml\nkubectl -n reporting create job weekly-sales-manual --from=cronjob\u002Fweekly-sales-report\nkubectl -n reporting get jobs,pods -l job-name=weekly-sales-manual\nkubectl -n reporting logs job\u002Fweekly-sales-manual\nkubectl -n reporting get cronjob weekly-sales-report      # LAST SCHEDULE and ACTIVE columns\n",[41,1572,1573,1585,1607,1627,1641],{"__ignoreMap":39},[44,1574,1575,1577,1580,1583],{"class":46,"line":47},[44,1576,702],{"class":50},[44,1578,1579],{"class":54}," apply",[44,1581,1582],{"class":326}," -f",[44,1584,732],{"class":54},[44,1586,1587,1589,1592,1595,1598,1601,1604],{"class":46,"line":64},[44,1588,702],{"class":50},[44,1590,1591],{"class":326}," -n",[44,1593,1594],{"class":54}," reporting",[44,1596,1597],{"class":54}," create",[44,1599,1600],{"class":54}," job",[44,1602,1603],{"class":54}," weekly-sales-manual",[44,1605,1606],{"class":326}," --from=cronjob\u002Fweekly-sales-report\n",[44,1608,1609,1611,1613,1615,1618,1621,1624],{"class":46,"line":246},[44,1610,702],{"class":50},[44,1612,1591],{"class":326},[44,1614,1594],{"class":54},[44,1616,1617],{"class":54}," get",[44,1619,1620],{"class":54}," jobs,pods",[44,1622,1623],{"class":326}," -l",[44,1625,1626],{"class":54}," job-name=weekly-sales-manual\n",[44,1628,1629,1631,1633,1635,1638],{"class":46,"line":255},[44,1630,702],{"class":50},[44,1632,1591],{"class":326},[44,1634,1594],{"class":54},[44,1636,1637],{"class":54}," logs",[44,1639,1640],{"class":54}," job\u002Fweekly-sales-manual\n",[44,1642,1643,1645,1647,1649,1651,1654,1657],{"class":46,"line":266},[44,1644,702],{"class":50},[44,1646,1591],{"class":326},[44,1648,1594],{"class":54},[44,1650,1617],{"class":54},[44,1652,1653],{"class":54}," cronjob",[44,1655,1656],{"class":54}," weekly-sales-report",[44,1658,1659],{"class":67},"      # LAST SCHEDULE and ACTIVE columns\n",[10,1661,1662,1665,1666,1669,1670,1673,1674,1678],{},[41,1663,1664],{},"create job --from=cronjob\u002F..."," runs the CronJob's template once, immediately — the way to test a new image or a changed secret without waiting for Monday. ",[41,1667,1668],{},"get cronjob"," shows when it last ran and whether a run is active. A job that never starts is usually an image pull error or a missing secret, visible in ",[41,1671,1672],{},"kubectl describe pod","; one that starts and fails shows its reason in the logs. ",[23,1675,1677],{"href":1676},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run\u002F","Detect a Scheduled Report That Did Not Run"," covers alerting when a run is missed altogether.",[29,1680,1682],{"id":1681},"performance-notes","Performance notes",[10,1684,1685,1686,1689,1690,1693,1694,1696],{},"The resource requests decide where the pod can be scheduled and how much of a node it reserves; set them from a measured run — ",[41,1687,1688],{},"kubectl top pod"," during a manual run shows real usage — rather than guessing. The memory limit is a hard ceiling: a pandas job that exceeds it is killed with ",[41,1691,1692],{},"OOMKilled",", which shows in ",[41,1695,1672],{}," and counts as a failed attempt. Workbook-building is usually single-threaded, so more than one CPU rarely helps; more memory often does.",[29,1698,1700],{"id":1699},"common-pitfalls","Common pitfalls",[619,1702,1703,1712,1721,1729,1738],{},[622,1704,1705,1708,1709,1711],{},[625,1706,1707],{},"Overlapping runs."," Set ",[41,1710,644],{},"; the default allows two copies at once.",[622,1713,1714,1717,1718,1720],{},[625,1715,1716],{},"No time zone."," Without ",[41,1719,633],{}," the schedule is in the controller's zone, usually UTC.",[622,1722,1723,1708,1726,1728],{},[625,1724,1725],{},"Unlimited runtime.",[41,1727,668],{},"; a hung pod otherwise lives until someone notices.",[622,1730,1731,1737],{},[625,1732,1733,1734,1736],{},"The ",[41,1735,691],{}," tag."," Pin image versions so runs are reproducible.",[622,1739,1740,1743],{},[625,1741,1742],{},"Typos in field names."," Unknown fields are dropped with at most a warning; validate strictly in CI.",[29,1745,1747],{"id":1746},"conclusion","Conclusion",[10,1749,1750,1751,1753,1754,1756,1757,1759,1760,1762,1763,1766],{},"Write the CronJob with a five-field ",[41,1752,629],{}," and an explicit ",[41,1755,633],{},", forbid concurrent runs, allow a bounded late start, and give the Job retries, a hard time limit and automatic clean-up. Feed credentials from a Secret, pin the image version, validate the manifest strictly with ",[41,1758,79],{}," before applying, make the script exit non-zero on failure and stop on ",[41,1761,1229],{},", and test it with ",[41,1764,1765],{},"kubectl create job --from=cronjob\u002F...",".",[29,1768,1770],{"id":1769},"frequently-asked-questions","Frequently asked questions",[10,1772,1773,1776],{},[625,1774,1775],{},"How do I schedule a Python script in Kubernetes?","\nPackage it in a container image and create a CronJob with a cron schedule whose jobTemplate runs that image. Kubernetes creates a Job, and a pod, at each scheduled time.",[10,1778,1779,1782],{},[625,1780,1781],{},"How do I stop Kubernetes CronJob runs from overlapping?","\nSet concurrencyPolicy: Forbid on the CronJob. A run that is due while the previous one is still active is then skipped.",[10,1784,1785,1788],{},[625,1786,1787],{},"How do I run a CronJob immediately to test it?","\nUse kubectl create job with --from=cronjob\u002FNAME and a new job name. It runs the CronJob's job template once, right away.",[10,1790,1791,1794],{},[625,1792,1793],{},"Does a Kubernetes CronJob support time zones?","\nYes. Set spec.timeZone to an IANA zone name such as Europe\u002FLondon; the schedule is then evaluated in that zone, including daylight-saving changes.",[29,1796,1798],{"id":1797},"related","Related",[619,1800,1801,1807,1814,1821],{},[622,1802,1803,1804,1806],{},"Up one level: ",[23,1805,26],{"href":25}," — every way to run reports on a schedule.",[622,1808,1809,1813],{},[23,1810,1812],{"href":1811},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-on-google-cloud-run-jobs\u002F","Run an Excel Report on Google Cloud Run Jobs"," — the managed alternative on Google Cloud.",[622,1815,1816,1820],{},[23,1817,1819],{"href":1818},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-overlapping-runs-of-an-excel-report-job\u002F","Prevent Overlapping Runs of an Excel Report Job"," — overlap protection outside Kubernetes.",[622,1822,1823,1827],{},[23,1824,1826],{"href":1825},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Fchoose-a-scheduler-for-python-excel-reports\u002F","Choose a Scheduler for Python Excel Reports"," — when Kubernetes is worth it.",[1829,1830,1831],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sx1sP, html code.shiki .sx1sP{--shiki-default:#22863A;--shiki-dark:#72F088}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}",{"title":39,"searchDepth":64,"depth":64,"links":1833},[1834,1835,1836,1837,1838,1839,1840,1841,1842,1843,1844],{"id":31,"depth":64,"text":32},{"id":209,"depth":64,"text":210},{"id":695,"depth":64,"text":696},{"id":907,"depth":64,"text":908},{"id":1222,"depth":64,"text":1223},{"id":1566,"depth":64,"text":1567},{"id":1681,"depth":64,"text":1682},{"id":1699,"depth":64,"text":1700},{"id":1746,"depth":64,"text":1747},{"id":1769,"depth":64,"text":1770},{"id":1797,"depth":64,"text":1798},"2026-09-29","Schedule a containerised Python Excel report on a Kubernetes cluster: a CronJob manifest with a time zone, no overlapping runs, a deadline for late starts, retries and a hard time limit, credentials from a Secret, output to object storage, clean-up of finished jobs, a strict schema check before applying, and manual test runs.","md",[1849,1851,1853,1855],{"q":1775,"a":1850},"Package it in a container image and create a CronJob with a cron schedule whose jobTemplate runs that image. Kubernetes creates a Job, and a pod, at each scheduled time.",{"q":1781,"a":1852},"Set concurrencyPolicy: Forbid on the CronJob. A run that is due while the previous one is still active is then skipped.",{"q":1787,"a":1854},"Use kubectl create job with --from=cronjob\u002FNAME and a new job name. It runs the CronJob's job template once, right away.",{"q":1793,"a":1856},"Yes. Set spec.timeZone to an IANA zone name such as Europe\u002FLondon; the schedule is then evaluated in that zone, including daylight-saving changes.","run an excel report as a kubernetes cronjob",null,{"breadcrumb":1860},[1861,1864,1867],{"name":1862,"item":1863},"Home","\u002F",{"name":1865,"item":1866},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":26,"item":25},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob",{"title":1870,"description":1871},"Schedule a Python Excel Report with a Kubernetes CronJob","Run an Excel report as a Kubernetes CronJob: schedule and timeZone, concurrencyPolicy Forbid, startingDeadlineSeconds, backoffLimit, activeDeadlineSeconds, ttlSecondsAfterFinished, Secret env vars, strict validation with kubernetes-validate and kubectl create job --from.","run-an-excel-report-as-a-kubernetes-cronjob","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-as-a-kubernetes-cronjob\u002Findex",[1875,1876],"scheduling","reliable-jobs","how-to","hE0jtmpaz2VJ7ff0YLRnbe-sQbHCcz6Gy4umC1wSq3k",[1880,1884],{"title":1881,"path":1882,"stem":1883,"children":-1},"Run a Report on the Last Business Day of the Month","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-report-on-the-last-business-day-of-the-month","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-a-report-on-the-last-business-day-of-the-month\u002Findex",{"title":1885,"path":1886,"stem":1887,"children":-1},"Run a Python Excel Report in AWS Lambda","\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-in-aws-lambda","automating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron\u002Frun-an-excel-report-in-aws-lambda\u002Findex",[1889,1891],{"path":1890,"title":1865},"\u002Fautomating-reporting-workflows",{"path":1892,"title":26},"\u002Fautomating-reporting-workflows\u002Fscheduling-python-excel-scripts-with-cron",[],[1895,1898,1901,1904,1907,1910],{"path":1896,"title":1897,"kind":1877},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fdownload-new-excel-files-from-an-sftp-server","Download New Excel Files from an SFTP Server",{"path":1899,"title":1900,"kind":1877},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Fread-excel-attachments-into-a-database-nightly","Read Excel Attachments into a Database Nightly",{"path":1902,"title":1903,"kind":1877},"\u002Fadvanced-data-transformation-and-cleaning\u002Fmoving-data-between-excel-and-databases\u002Frefresh-an-excel-report-from-a-database-on-a-schedule","Refresh an Excel Report from a Database on a Schedule",{"path":1905,"title":1906,"kind":1877},"\u002Fautomating-reporting-workflows\u002Fautomating-excel-online-with-microsoft-graph\u002Frun-a-report-when-an-excel-file-changes-in-onedrive","Run a Report When an Excel File Changes in OneDrive",{"path":1908,"title":1909,"kind":1877},"\u002Fautomating-reporting-workflows\u002Femailing-excel-reports-with-smtplib\u002Fsend-report-emails-at-each-recipients-local-time","Send Report Emails at Each Recipient's Local Time",{"path":1911,"title":1677,"kind":1877},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run",1790972138812]