European champions Italy finished second in Group C, behind England and level on 14 points with third-placed Ukraine, but with a better head-to-head record after a tense affair that could have gone either way.
The Italians ended six points behind England, who had sealed qualification after beating Italy 3-1 at Wembley in October.
Ukraine, who have never beaten Italy, had strong claims for a penalty after Bryan Cristante's clumsy challenge on Mykhailo Mudryk in stoppage time, but the referee waved the appeals and VAR did not come to their rescue.
They can still book a spot at a fourth consecutive European Championship via the playoffs in March. The playoff draw will take place on Thursday in Nyon.
The match got off to a lively start but both goalkeepers - Italy's Gianluigi Donnarumma and Ukraine's Anatoliy Trubin - held firm in an entertaining clash at BayArena in Leverkusen.
Italy, the defending champions who will make their eighth consecutive appearance at the Euros, had most possession and created more chances but Ukraine remained composed as they looked to counter-attack.
Federico Chiesa had a chance to break the deadlock in the seventh minute but his effort from inside the box flew narrowly over the bar before Donnarumma denied Ukraine an opener by keeping out Georgiy Sudakov's low shot.
Defender Giovanni Di Lorenzo then nearly nodded home for Italy from a corner just before the half-hour mark before Trubin pulled off a stunning save to deny Davide Frattesi.
Italy continued to dominate after the break but Ukraine's probing increased and Mudryk came close in the 66th minute only to be denied by Donnarumma who pulled off another fine save. The Chelsea winger looked to have won a penalty in the final stages, but Italy survived.
Additional reporting by Reuters.